diff --git a/example/lib/main.dart b/example/lib/main.dart index d602cec..4062890 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:developer'; +import 'dart:math' hide log; +import 'package:badges/badges.dart'; import 'package:example/app_controller.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -106,19 +108,30 @@ class _HomePageState extends ConsumerState { selectedIcon: const Icon( Icons.home, size: 26, + ), + badge: const NavbarBadge( + badgeText: "11", + showBadge: true, )), NavbarItem(Icons.shopping_bag_outlined, 'Products', backgroundColor: colors[1], selectedIcon: const Icon( Icons.shopping_bag, size: 26, + ), + badge: const NavbarBadge( + badgeText: "8", + showBadge: true, )), NavbarItem(Icons.person_outline, 'Me', backgroundColor: colors[2], selectedIcon: const Icon( Icons.person, size: 26, - )), + ), + // dot badge + badge: const NavbarBadge( + badgeText: "", showBadge: true, color: Colors.amber)), NavbarItem(Icons.settings_outlined, 'Settings', backgroundColor: colors[0], selectedIcon: const Icon( @@ -755,11 +768,88 @@ class _SettingsState extends State { index = x; }); appSetting.changeThemeSeed(themeColorSeed[index.toInt()]); - }) + }), + const SizedBox( + height: 40, + ), + ElevatedButton( + onPressed: () { + showRandomBadges(); + }, + child: const Text('Show Random Badges')) ], ), )); } + + showRandomBadges() { + var anims = [ + BadgeAnimation.fade, + BadgeAnimation.rotation, + BadgeAnimation.scale, + BadgeAnimation.size, + BadgeAnimation.slide + ]; + var pos = [ + BadgePosition.bottomEnd, + BadgePosition.bottomStart, + BadgePosition.topEnd, + BadgePosition.topStart + ]; + int r = Random().nextInt(100); + var b = Random().nextBool(); + NavbarNotifier.updateBadge( + 0, + NavbarBadge( + badgeText: "${b ? r : ""}", + color: b + ? null + : themeColorSeed[Random().nextInt(themeColorSeed.length)], + position: pos[Random().nextInt(pos.length)](), + showBadge: Random().nextInt(5) > 1, + badgeAnimation: anims[Random().nextInt(anims.length)](), + animationDuration: + Duration(milliseconds: (Random().nextInt(5) + 5) * 600))); + b = Random().nextBool(); + NavbarNotifier.updateBadge( + 1, + NavbarBadge( + badgeText: "${b ? r : ""}", + position: pos[Random().nextInt(pos.length)](), + color: b + ? null + : themeColorSeed[Random().nextInt(themeColorSeed.length)], + showBadge: Random().nextInt(5) > 1, + badgeAnimation: anims[Random().nextInt(anims.length)](), + animationDuration: + Duration(milliseconds: (Random().nextInt(5) + 5) * 600))); + b = Random().nextBool(); + NavbarNotifier.updateBadge( + 2, + NavbarBadge( + badgeText: "${b ? r : ""}", + position: pos[Random().nextInt(pos.length)](), + color: b + ? null + : themeColorSeed[Random().nextInt(themeColorSeed.length)], + showBadge: Random().nextInt(5) > 1, + badgeAnimation: anims[Random().nextInt(anims.length)](), + animationDuration: + Duration(milliseconds: (Random().nextInt(5) + 5) * 600))); + b = Random().nextBool(); + NavbarNotifier.updateBadge( + 3, + NavbarBadge( + badgeText: "${b ? r : ""}", + position: pos[Random().nextInt(pos.length)](), + color: b + ? null + : themeColorSeed[Random().nextInt(themeColorSeed.length)], + showBadge: Random().nextInt(5) > 1, + badgeAnimation: anims[Random().nextInt(anims.length)](), + animationDuration: + Duration(milliseconds: (Random().nextInt(5) + 5) * 600))); + } } class ProfileEdit extends StatelessWidget { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 3b1fde5..1fc6337 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: path: ../ flutter: sdk: flutter + badges: cupertino_icons: ^1.0.2 flutter_riverpod: ^2.4.9 diff --git a/lib/navbar_router.dart b/lib/navbar_router.dart index 6389c04..427f23a 100644 --- a/lib/navbar_router.dart +++ b/lib/navbar_router.dart @@ -4,3 +4,4 @@ export 'src/navbar_router.dart'; export 'src/navbar_notifier.dart'; export 'src/navbar_decoration.dart'; export 'src/navigate.dart'; +export 'src/navbar_badge.dart'; diff --git a/lib/src/animated_navbar.dart b/lib/src/animated_navbar.dart index 825a750..f245886 100644 --- a/lib/src/animated_navbar.dart +++ b/lib/src/animated_navbar.dart @@ -10,6 +10,45 @@ const double kFloatingNavbarHeight = 60.0; /// The height of the navbar based on the [NavbarType] double kNavbarHeight = 0.0; +/// Function to build badges, using index and child from the [NavbarNotifier.badges] list (given by user) +Widget buildBadge( + /// Current index of the navbar + int index, + + /// The navbar icon + Widget child, +) { + return badges.Badge( + key: NavbarNotifier.badges[index].key, + position: NavbarNotifier.badges[index].position ?? + (NavbarNotifier.badges[index].badgeText.isNotEmpty + ? badges.BadgePosition.topEnd(top: -15, end: -15) + : badges.BadgePosition.topEnd()), + badgeAnimation: NavbarNotifier.badges[index].badgeAnimation ?? + badges.BadgeAnimation.slide( + animationDuration: NavbarNotifier.badges[index].animationDuration, + // disappearanceFadeAnimationDuration: Duration(milliseconds: 200), + // curve: Curves.easeInCubic, + ), + ignorePointer: NavbarNotifier.badges[index].ignorePointer, + stackFit: NavbarNotifier.badges[index].stackFit, + onTap: NavbarNotifier.badges[index].onTap, + showBadge: NavbarNotifier.badges[index].showBadge, + badgeStyle: badges.BadgeStyle( + badgeColor: NavbarNotifier.badges[index].color ?? Colors.white, + ), + badgeContent: NavbarNotifier.badges[index].badgeContent ?? + Text( + NavbarNotifier.badges[index].badgeText, + style: NavbarNotifier.badges[index].badgeTextStyle ?? + TextStyle( + color: NavbarNotifier.badges[index].textColor ?? Colors.black, + fontSize: 9), + ), + child: child, + ); +} + class _AnimatedNavBar extends StatefulWidget { const _AnimatedNavBar( {Key? key, @@ -335,12 +374,13 @@ class _AnimatedNavBarState extends State<_AnimatedNavBar> extended: navigationRailDefaultDecoration.isExtended, backgroundColor: navigationRailDefaultDecoration.backgroundColor ?? theme.colorScheme.surface, - destinations: widget.menuItems.map((NavbarItem menuItem) { - return NavigationRailDestination( - icon: Icon(menuItem.iconData), - label: Text(menuItem.text), - ); - }).toList(), + destinations: [ + for (int i = 0; i < widget.menuItems.length; i++) + NavigationRailDestination( + icon: buildBadge(i, Icon(widget.menuItems[i].iconData)), + label: Text(widget.menuItems[i].text), + ) + ], selectedIndex: NavbarNotifier.currentIndex); } @@ -441,12 +481,13 @@ class StandardNavbarState extends State { BottomNavigationBarItem( backgroundColor: items[index].backgroundColor, icon: _selectedIndex == index - ? items[index].selectedIcon ?? - Icon( - items[index].iconData, - ) - : Icon( - items[index].iconData, + ? buildBadge( + index, + items[index].selectedIcon ?? Icon(items[index].iconData), + ) + : buildBadge( + index, + Icon(items[index].iconData), ), label: items[index].text, ) @@ -592,13 +633,15 @@ class NotchedNavBarState extends State ), ], ), - child: widget.menuItems[NavbarNotifier.currentIndex].selectedIcon ?? - Icon( - widget.menuItems[NavbarNotifier.currentIndex].iconData, - color: widget.decoration.selectedIconColor, - size: (widget.decoration.selectedIconTheme?.size ?? 24.0) * - scaleAnimation.value, - )); + child: buildBadge( + NavbarNotifier.currentIndex, + widget.menuItems[NavbarNotifier.currentIndex].selectedIcon ?? + Icon( + widget.menuItems[NavbarNotifier.currentIndex].iconData, + color: widget.decoration.selectedIconColor, + size: (widget.decoration.selectedIconTheme?.size ?? 24.0) * + scaleAnimation.value, + ))); } @override @@ -651,6 +694,7 @@ class NotchedNavBarState extends State alignment: Alignment.center, height: 80, child: MenuTile( + index: i, item: widget.menuItems[i], decoration: widget.decoration, ), @@ -667,8 +711,13 @@ class NotchedNavBarState extends State class MenuTile extends StatelessWidget { final NavbarDecoration decoration; final NavbarItem item; + final int index; - const MenuTile({super.key, required this.item, required this.decoration}); + const MenuTile( + {super.key, + required this.item, + required this.decoration, + required this.index}); @override Widget build(BuildContext context) { @@ -676,11 +725,13 @@ class MenuTile extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - item.iconData, - color: - decoration.unselectedIconColor ?? decoration.unselectedItemColor, - ), + buildBadge( + index, + Icon( + item.iconData, + color: decoration.unselectedIconColor ?? + decoration.unselectedItemColor, + )), const SizedBox( height: 6, ), @@ -836,14 +887,18 @@ class M3NavBarState extends State { indicatorColor: widget.decoration.indicatorColor, indicatorShape: widget.decoration.indicatorShape, labelBehavior: widget.labelBehavior, - destinations: widget.items.map((e) { - return NavigationDestination( - tooltip: e.text, - icon: Icon(e.iconData), - label: e.text, - selectedIcon: e.selectedIcon ?? Icon(e.iconData), - ); - }).toList(), + destinations: [ + for (int i = 0; i < widget.items.length; i++) + NavigationDestination( + tooltip: widget.items[i].text, + icon: buildBadge(i, Icon(widget.items[i].iconData)), + label: widget.items[i].text, + selectedIcon: buildBadge( + i, + widget.items[i].selectedIcon ?? + Icon(widget.items[i].iconData)), + ) + ], selectedIndex: NavbarNotifier.currentIndex, onDestinationSelected: (int index) => widget.onItemTapped!(index)), ), @@ -985,9 +1040,11 @@ class FloatingNavbarState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ _selectedIndex == i - ? widget.items[i].selectedIcon ?? - _unselectedIcon(i) - : _unselectedIcon(i), + ? buildBadge( + i, + widget.items[i].selectedIcon ?? + _unselectedIcon(i)) + : buildBadge(i, _unselectedIcon(i)), if (widget.decoration.showSelectedLabels! && widget.index == i) Text( diff --git a/lib/src/navbar_badge.dart b/lib/src/navbar_badge.dart new file mode 100644 index 0000000..6759946 --- /dev/null +++ b/lib/src/navbar_badge.dart @@ -0,0 +1,275 @@ +import 'package:badges/badges.dart'; +import 'package:flutter/material.dart'; + +/// A customized badge/dot class for NavbarRouter. +/// Based on [badges] package of flutter. +class NavbarBadge { + /// Your badge content, can be number (as string) of text. + /// Please choose either [badgeText] or [badgeContent]. + /// + /// If **[badgeContent]** is not null, **[badgeText]** will be **ignored**. + final String badgeText; + + /// Text style for badge + final TextStyle? badgeTextStyle; + + /// Content inside badge. Please choose either [badgeText] or [badgeContent]. + /// + /// If not null, **[badgeText]** will be **ignored**. + final Widget? badgeContent; + + /// Allows you to hide or show entire badge. + /// The default value is false. + final bool showBadge; + + /// Duration of the badge animations when the [badgeContent] changes. + /// The default value is Duration(milliseconds: 500). + final Duration animationDuration; + + /// Background color of the badge. + /// The default value is white. + final Color? color; + + /// Text color of the badge. + /// The default value is black. + final Color? textColor; + + /// Contains all badge style properties. + /// + /// Allows to set the shape to this [badgeContent]. + /// The default value is [BadgeShape.circle]. + /// ``` + /// final BadgeShape shape; + /// ``` + /// Allows to set border radius to this [badgeContent]. + /// The default value is [BorderRadius.zero]. + /// ``` + /// final BorderRadius borderRadius; + /// ``` + /// Background color of the badge. + /// If [gradient] is not null, this property will be ignored. + /// ``` + /// final Color badgeColor; + /// ``` + /// Allows to set border side to this [badgeContent]. + /// The default value is [BorderSide.none]. + /// ``` + /// final BorderSide borderSide; + /// ``` + /// The size of the shadow below the badge. + /// ``` + /// final double elevation; + /// ``` + /// Background gradient color of the badge. + /// Will be used over [badgeColor] if not null. + /// ``` + /// final BadgeGradient? badgeGradient; + /// ``` + /// Background gradient color of the border badge. + /// Will be used over [borderSide.color] if not null. + /// ``` + /// final BadgeGradient? borderGradient; + /// ``` + /// Specifies padding/**size** for [badgeContent]. + /// The default value is EdgeInsets.all(5.0). + /// ``` + /// final EdgeInsetsGeometry padding; + /// ``` + final BadgeStyle badgeStyle; + + /// Contains all badge animation properties. + /// + /// True to animate badge on [badgeContent] change. + /// False to disable animation. + /// Default value is true. + /// ``` + /// final bool toAnimate; + /// ``` + /// Duration of the badge animations when the [badgeContent] changes. + /// The default value is Duration(milliseconds: 500). + /// ``` + /// final Duration animationDuration; + /// ``` + /// Duration of the badge appearance and disappearance fade animations. + /// Fade animation is created with [AnimatedOpacity]. + /// + /// Some of the [BadgeAnimationType] cannot be used for appearance and disappearance animation. + /// E.g. [BadgeAnimationType.scale] can be used, but [BadgeAnimationType.rotation] cannot be used. + /// That is why we need fade animation and duration for it when it comes to appearance and disappearance + /// of these "non-disappearing" animations. + /// + /// There is a thing: you need this duration to NOT be longer than [animationDuration] + /// if you want to use the basic animation as appearance and disappearance animation. + /// + /// Set this to zero to skip the badge appearance and disappearance animations + /// The default value is Duration(milliseconds: 200). + /// ``` + /// final Duration disappearanceFadeAnimationDuration; + /// ``` + /// Type of the animation for badge + /// The default value is [BadgeAnimationType.slide]. + /// ``` + /// final BadgeAnimationType animationType; + /// ``` + /// Make it true to have infinite animation + /// False to have animation only when [badgeContent] is changed + /// The default value is false + /// ``` + /// final bool loopAnimation; + /// ``` + /// Controls curve of the animation + /// ``` + /// final Curve curve; + /// ``` + /// Used only for [SizeTransition] animation + /// The default value is Axis.horizontal + /// ``` + /// final Axis? sizeTransitionAxis; + /// ``` + /// Used only for [SizeTransition] animation + /// The default value is 1.0 + /// ``` + /// final double? sizeTransitionAxisAlignment; + /// ``` + /// Used only for [SlideTransition] animation + /// The default value is + /// ``` + /// SlideTween( + /// begin: const Offset(-0.5, 0.9), + /// end: const Offset(0.0, 0.0), + /// ); + /// ``` + /// ``` + /// final SlideTween? slideTransitionPositionTween; + /// ``` + /// Used only for changing color animation. + /// The default value is [Curves.linear] + /// ``` + /// final Curve colorChangeAnimationCurve; + /// ``` + /// Used only for changing color animation. + /// The default value is [Duration.zero], meaning that + /// no animation will be applied to color change by default. + /// ``` + /// final Duration colorChangeAnimationDuration; + /// ``` + /// This one is interesting. + /// Some animations use [AnimatedOpacity] to animate appearance and disappearance of the badge. + /// E.x. how would you animate disappearance of [BadgeAnimationType.rotation]? We should use [AnimatedOpacity] for that. + /// But sometimes you may need to disable this fade appearance/disappearance animation. + /// You can do that by setting this to false. + /// Using disappearanceFadeAnimationDuration: Duration.zero is not correct, this will remove the animation entirely + /// ``` + /// final bool appearanceDisappearanceFadeAnimationEnabled; + /// ``` + final BadgeAnimation? badgeAnimation; + + /// Allows to set custom position of badge according to [child]. + /// If [child] is null, it doesn't make sense to use it. + final BadgePosition? position; + + /// Can make your [badgeContent] interactive. + /// The default value is false. + /// Make it true to make badge intercept all taps + /// Make it false and all taps will be passed through the badge + final bool ignorePointer; + + /// Allows to edit fit parameter to [Stack] widget. + /// The default value is [StackFit.loose]. + final StackFit stackFit; + + /// Will be called when you tap on the badge + /// Important: if the badge is outside of the child + /// the additional padding will be applied to make the full badge clickable + final Function()? onTap; + + final Key? key; + + /// Use padding of [badgeStyle] or fontSize of [badgeTextStyle] to change size of the badge/dot. + const NavbarBadge({ + this.key, + this.badgeText = "", + this.showBadge = false, + this.animationDuration = const Duration(milliseconds: 500), + this.color = Colors.white, + this.textColor, + this.badgeStyle = const BadgeStyle(), + this.badgeAnimation = const BadgeAnimation.slide(), + this.position, + this.ignorePointer = false, + this.stackFit = StackFit.loose, + this.onTap, + this.badgeContent, + this.badgeTextStyle, + }); + + @override + int get hashCode => + badgeText.hashCode ^ + showBadge.hashCode ^ + animationDuration.hashCode ^ + color.hashCode ^ + textColor.hashCode ^ + badgeStyle.hashCode ^ + badgeAnimation.hashCode ^ + position.hashCode ^ + ignorePointer.hashCode ^ + stackFit.hashCode ^ + onTap.hashCode ^ + badgeContent.hashCode ^ + badgeStyle.hashCode; + + @override + bool operator ==(Object other) { + return identical(this, other) || + other is NavbarBadge && + runtimeType == other.runtimeType && + badgeText == other.badgeText && + showBadge == other.showBadge && + animationDuration == other.animationDuration && + color == other.color && + textColor == other.textColor && + badgeStyle == other.badgeStyle && + badgeAnimation == other.badgeAnimation && + position == other.position && + ignorePointer == other.ignorePointer && + stackFit == other.stackFit && + onTap == other.onTap && + badgeContent == other.badgeContent && + badgeStyle == other.badgeStyle; + } + + NavbarBadge copyWith({ + String? badgeText, + TextStyle? badgeTextStyle, + Widget? badgeContent, + bool? showBadge, + Duration? animationDuration, + Color? color, + Color? textColor, + BadgeStyle? badgeStyle, + BadgeAnimation? badgeAnimation, + BadgePosition? position, + bool? ignorePointer, + StackFit? stackFit, + Function()? onTap, + Key? key, + }) { + return NavbarBadge( + badgeText: badgeText ?? this.badgeText, + badgeTextStyle: badgeTextStyle ?? this.badgeTextStyle, + badgeContent: badgeContent ?? this.badgeContent, + showBadge: showBadge ?? this.showBadge, + animationDuration: animationDuration ?? this.animationDuration, + color: color ?? this.color, + textColor: textColor ?? this.textColor, + badgeStyle: badgeStyle ?? this.badgeStyle, + badgeAnimation: badgeAnimation ?? this.badgeAnimation, + position: position ?? this.position, + ignorePointer: ignorePointer ?? this.ignorePointer, + stackFit: stackFit ?? this.stackFit, + onTap: onTap ?? this.onTap, + key: key ?? this.key, + ); + } +} diff --git a/lib/src/navbar_decoration.dart b/lib/src/navbar_decoration.dart index 2bfb49f..2c05972 100644 --- a/lib/src/navbar_decoration.dart +++ b/lib/src/navbar_decoration.dart @@ -3,7 +3,10 @@ import 'package:navbar_router/navbar_router.dart'; class NavbarItem { const NavbarItem(this.iconData, this.text, - {this.backgroundColor, this.child, this.selectedIcon}); + {this.backgroundColor, + this.child, + this.selectedIcon, + this.badge = const NavbarBadge()}); /// IconData for the navbar item final IconData iconData; @@ -22,6 +25,9 @@ class NavbarItem { /// Widget to show when the item is selected final Widget? selectedIcon; + /// Your initial badge configuration for this item, this is totally optional + final NavbarBadge badge; + @override bool operator ==(Object other) => identical(this, other) || @@ -31,7 +37,8 @@ class NavbarItem { text == other.text && child.runtimeType == other.child.runtimeType && selectedIcon.runtimeType == other.selectedIcon.runtimeType && - backgroundColor == other.backgroundColor; + backgroundColor == other.backgroundColor && + badge == other.badge; @override int get hashCode => @@ -39,7 +46,8 @@ class NavbarItem { text.hashCode ^ child.hashCode ^ selectedIcon.hashCode ^ - backgroundColor.hashCode; + backgroundColor.hashCode ^ + badge.hashCode; } /// Decoration class for the navbar [NavbarType.standard] diff --git a/lib/src/navbar_notifier.dart b/lib/src/navbar_notifier.dart index c58a2b4..9365f0e 100644 --- a/lib/src/navbar_notifier.dart +++ b/lib/src/navbar_notifier.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:navbar_router/src/navbar_router.dart'; +import 'package:navbar_router/navbar_router.dart'; class NavbarNotifier extends ChangeNotifier { static final NavbarNotifier _singleton = NavbarNotifier._internal(); @@ -30,16 +30,50 @@ class NavbarNotifier extends ChangeNotifier { static List> _keys = []; + /// Set to true will hide the badges when the tap on the navbar icon. + static bool hideBadgeOnPageChanged = true; + + /// List of badges of the navbar + static List get badges => _badges; + static List _badges = []; + + /// Use to update a badge using its [index], e.g: update the number, text... + /// + /// If you want to hide badges on a specific index, use [makeBadgeVisible] + /// + static void updateBadge(int index, NavbarBadge badge) { + if (index < 0 || index >= length) return; + _badges[index] = badge; + + _singleton.notify(); + } + + /// Use to set the visibility of a badge using its [index]. + static void makeBadgeVisible(int index, bool visible) { + if (index < 0 || index >= length) return; + _badges[index] = _badges[index].copyWith(showBadge: visible); + + _singleton.notify(); + } + static void setKeys(List> value) { _keys = value; } + /// The only place that init the badges. + static void setBadges(List? badgeList) { + if (badgeList != null) { + _badges = badgeList; + } + } + static final List _indexChangeListeners = []; static List> get keys => _keys; static set index(int x) { _index = x; + if (hideBadgeOnPageChanged) makeBadgeVisible(x, false); if (_navbarStackHistory.contains(x)) { _navbarStackHistory.remove(x); } @@ -242,5 +276,6 @@ class NavbarNotifier extends ChangeNotifier { _keys.clear(); _index = null; _length = null; + _badges.clear(); } } diff --git a/lib/src/navbar_router.dart b/lib/src/navbar_router.dart index f9e9ba3..55eb836 100644 --- a/lib/src/navbar_router.dart +++ b/lib/src/navbar_router.dart @@ -1,3 +1,4 @@ +import 'package:badges/badges.dart' as badges; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:navbar_router/navbar_router.dart'; @@ -148,6 +149,9 @@ class NavbarRouter extends StatefulWidget { /// defaults to the first item in the list of [NavbarItems] final int initialIndex; + /// Set to true will hide the badges when the tap on the navbar icon. + final bool hideBadgeOnPageChanged; + /// Take a look at the [readme](https://github.com/maheshmnj/navbar_router) for more information on how to use this package. /// /// Please help me improve this package. @@ -170,6 +174,7 @@ class NavbarRouter extends StatefulWidget { this.destinationAnimationDuration = 300, this.backButtonBehavior = BackButtonBehavior.exit, this.onCurrentTabClicked, + this.hideBadgeOnPageChanged = true, this.onBackButtonPressed}) : assert(destinations.length >= 2, "Destinations length must be greater than or equal to 2"), @@ -193,15 +198,25 @@ class _NavbarRouterState extends State void initialize({bool isUpdate = false}) { NavbarNotifier.length = widget.destinations.length; + // init badge + List badges = []; for (int i = 0; i < NavbarNotifier.length; i++) { final navbaritem = widget.destinations[i].navbarItem; keys.add(GlobalKey()); items.add(navbaritem); + badges.add(navbaritem.badge); } NavbarNotifier.setKeys(keys); + + // set badge list here + NavbarNotifier.setBadges(badges); + NavbarNotifier.hideBadgeOnPageChanged = widget.hideBadgeOnPageChanged; + if (!isUpdate) { initAnimation(); NavbarNotifier.index = widget.initialIndex; + // re-enable the initial badge that was hidden on setting NavbarNotifier.index + NavbarNotifier.makeBadgeVisible(NavbarNotifier.currentIndex, true); } } @@ -354,6 +369,10 @@ class _NavbarRouterState extends State if (widget.onCurrentTabClicked != null) { widget.onCurrentTabClicked!(); } + if (widget.hideBadgeOnPageChanged) { + NavbarNotifier.makeBadgeVisible( + NavbarNotifier.currentIndex, false); + } } else { NavbarNotifier.index = x; if (widget.onChanged != null) { diff --git a/pubspec.yaml b/pubspec.yaml index 1170c18..553f35a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ environment: dependencies: flutter: sdk: flutter + badges: ^3.1.2 dev_dependencies: flutter_test: diff --git a/test/navbar_router_test.dart b/test/navbar_router_test.dart index a4a4f8a..56300f1 100644 --- a/test/navbar_router_test.dart +++ b/test/navbar_router_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; +import 'package:badges/badges.dart' as badges; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:navbar_router/navbar_router.dart'; @@ -28,6 +29,7 @@ extension FindIcon on IconData { } void main() { + // updated test items with badges const List items = [ NavbarItem(Icons.home_outlined, 'Home', backgroundColor: mediumPurple, @@ -35,6 +37,11 @@ void main() { key: Key("HomeIconSelected"), Icons.home, size: 26, + ), + badge: NavbarBadge( + key: Key("TwoDigitBadge"), + badgeText: "10", + showBadge: true, )), NavbarItem(Icons.shopping_bag_outlined, 'Products', backgroundColor: Colors.orange, @@ -42,6 +49,11 @@ void main() { Icons.shopping_bag, key: Key("ProductsIconSelected"), size: 26, + ), + badge: NavbarBadge( + key: Key("OneDigitBadge"), + badgeText: "8", + showBadge: true, )), NavbarItem(Icons.person_outline, 'Me', backgroundColor: Colors.teal, @@ -49,12 +61,22 @@ void main() { key: Key("MeIconSelected"), Icons.person, size: 26, + ), + badge: NavbarBadge( + key: Key("DotBadge1"), + showBadge: true, + color: Colors.amber, )), NavbarItem(Icons.settings_outlined, 'Settings', backgroundColor: Colors.red, selectedIcon: Icon( Icons.settings, size: 26, + ), + badge: NavbarBadge( + key: Key("DotBadge2"), + showBadge: true, + color: Colors.red, )), ]; @@ -146,8 +168,176 @@ void main() { expect(destinationFinder, findsOneWidget); } + // function containing all badge tests and subtests + badgeGroupTest({NavbarType type = NavbarType.standard}) { + badges.Badge findBadge(tester, index) { + return tester.widget(find.byKey(NavbarNotifier.badges[index].key!)) + as badges.Badge; + } + + // test color and visibility + testDot(tester, index) { + expect(find.byKey(NavbarNotifier.badges[index].key!), findsOneWidget); + + // test visibility + expect(findBadge(tester, index).showBadge, + NavbarNotifier.badges[index].showBadge); + + // test color + expect(findBadge(tester, index).badgeStyle.badgeColor, + NavbarNotifier.badges[index].color); + } + + /// Test badge + testBadgeWithText(tester, index) { + var textFind = find.text(NavbarNotifier.badges[index].badgeText); + expect(textFind, findsOneWidget); + + // test dot + testDot(tester, index); + + // compare the content of badge + Text text = tester.firstWidget(textFind); + expect(text.data, NavbarNotifier.badges[index].badgeText); + } + + desktopMode(tester) async { + await tester.pumpWidget(boilerplate(isDesktop: true, type: type)); + await tester.pumpAndSettle(); + expect(find.byType(NavigationRail), findsOneWidget); + expect(find.byType(BottomNavigationBar), findsNothing); + } + + testBadge(tester, index) async { + NavbarNotifier.badges[index].badgeText.isNotEmpty + ? testBadgeWithText(tester, index) + : testDot(tester, index); + NavbarNotifier.badges[index].badgeText.isNotEmpty + ? testBadgeWithText(tester, index) + : testDot(tester, index); + } + + testWidgets('Should build initial badges', (WidgetTester tester) async { + await tester.pumpWidget(boilerplate()); + // test visibility + testBadge(tester, 0); + testBadge(tester, 1); + testBadge(tester, 2); + testBadge(tester, 3); + }); + + testWidgets('Should allow to update badges dynamically', + (WidgetTester tester) async { + await tester.pumpWidget(boilerplate()); + + // test badge + testBadge(tester, 0); + // update the whole badge + NavbarNotifier.updateBadge( + 0, + const NavbarBadge( + key: Key("TwoDigitBadgeNew"), + badgeText: "11", + showBadge: true, + )); + await tester.pumpAndSettle(); + + testBadge(tester, 0); + + // hide the badge + NavbarNotifier.makeBadgeVisible(0, false); + await tester.pumpAndSettle(); + expect(NavbarNotifier.badges[0].showBadge, false); + testBadge(tester, 0); + }); + + testWidgets('Should allow to hide/show badges on demand', + (WidgetTester tester) async { + await tester.pumpWidget(boilerplate()); + + // test badge + testBadge(tester, 0); + + // hide all the badge + for (int i = 0; i < NavbarNotifier.length; i++) { + NavbarNotifier.makeBadgeVisible(i, false); + await tester.pumpAndSettle(); + expect(NavbarNotifier.badges[i].showBadge, false); + testBadge(tester, i); + } + + // show all the badge + for (int i = 0; i < NavbarNotifier.length; i++) { + NavbarNotifier.makeBadgeVisible(i, true); + await tester.pumpAndSettle(); + expect(NavbarNotifier.badges[i].showBadge, true); + testBadge(tester, i); + } + }); + + testWidgets('Desktop: should build initial badges', + (WidgetTester tester) async { + await desktopMode(tester); + // test visibility + testBadge(tester, 0); + testBadge(tester, 1); + testBadge(tester, 2); + testBadge(tester, 3); + }); + + testWidgets('Desktop: should allow to update badges dynamically', + (WidgetTester tester) async { + await desktopMode(tester); + + // test badge + testBadge(tester, 0); + // update the whole badge + NavbarNotifier.updateBadge( + 0, + const NavbarBadge( + key: Key("TwoDigitBadgeNew"), + badgeText: "11", + showBadge: true, + )); + await tester.pumpAndSettle(); + + testBadge(tester, 0); + + // hide the badge + NavbarNotifier.makeBadgeVisible(0, false); + await tester.pumpAndSettle(); + expect(NavbarNotifier.badges[0].showBadge, false); + testBadge(tester, 0); + }); + + testWidgets('Desktop: should allow to hide/show badges on demand', + (WidgetTester tester) async { + await desktopMode(tester); + + // test badge + testBadge(tester, 0); + + // hide all the badge + for (int i = 0; i < NavbarNotifier.length; i++) { + NavbarNotifier.makeBadgeVisible(i, false); + await tester.pumpAndSettle(); + expect(NavbarNotifier.badges[i].showBadge, false); + testBadge(tester, i); + } + + // show all the badge + for (int i = 0; i < NavbarNotifier.length; i++) { + NavbarNotifier.makeBadgeVisible(i, true); + await tester.pumpAndSettle(); + expect(NavbarNotifier.badges[i].showBadge, true); + testBadge(tester, i); + } + }); + } + group('Test NavbarType: NavbarType.standard ', () { - group('NavbarType.standard: should build destination and navbar items', () { + // test badges + group('Should build destination, navbar items, and badges', () { testWidgets('NavbarType.standard: should build destinations', (WidgetTester tester) async { final bottomNavigation = (BottomNavigationBar).typeX(); @@ -182,6 +372,10 @@ void main() { expect(find.text(items[2].text), findsOneWidget); }); + group('NavbarType.standard: badges test', () { + badgeGroupTest(); + }); + testWidgets( "NavbarType.standard: should allow updating navbar routes dynamically ", (WidgetTester tester) async { @@ -238,6 +432,7 @@ void main() { } }); }); + testWidgets('NavbarType.standard: default index must be zero', (WidgetTester tester) async { await tester.pumpWidget(boilerplate()); @@ -502,6 +697,10 @@ void main() { group('Test NavbarType: NavbarType.notched ', () { group('NavbarType.notched: should build destination and navbar items', () { + group('Badges test', () { + badgeGroupTest(type: NavbarType.notched); + }); + testWidgets('navbar_router should build destinations', (WidgetTester tester) async { final navbar = (NotchedNavBar).typeX(); @@ -817,6 +1016,9 @@ void main() { group( 'NavbarType.material3: should build destination and navbar items (Desktop)', () { + group('Badges test', () { + badgeGroupTest(type: NavbarType.material3); + }); testWidgets('navbar_router should build destinations', (WidgetTester tester) async { final navbar = (M3NavBar).typeX(); @@ -1358,6 +1560,9 @@ void main() { }); group('Test NavbarType: NavbarType.floating', () { + group('Badges test', () { + badgeGroupTest(type: NavbarType.floating); + }); testWidgets('NavbarType can be changed during runtime', (tester) async { NavbarType type = NavbarType.notched; await tester.pumpWidget(boilerplate(type: type));