BottomNavigationBar(CupertinoTabBar)에서 아이콘에 알림 뱃지 달아야 되는데, 은근 그냥 간단한 작업은 아니였어서 기록.
BottomNavigationBar 아니여도, 알림 뱃지(Notification Badge)를 달 때 따라해도 될 것 같고,
BottomNavigationBarItem 이용하는거니까 Material Style의 BottomNavigationBar든, Cupertino Style의 CupertinoTabBar든 상관 없이 참고해도 괜찮을 것 같다.
시작(기본 코드)
우선 기본적으로 이렇게 생긴 Navigation Bar에서 시작해 보겠다.

코드는 아래 참고.
class Homescreen extends StatefulWidget { @override _HomescreenState createState() => _HomescreenState(); } class _HomescreenState extends State<Homescreen> { CupertinoTabController _controller = CupertinoTabController(); @override void dispose() { _controller.dispose(); super.dispose(); } final tabInfo = [ _TabInfo( "Home", CupertinoIcons.house_fill, ), _TabInfo( "Friends", CupertinoIcons.person_2_fill, ), _TabInfo( "Chat", CupertinoIcons.ellipses_bubble_fill, ), _TabInfo( "Message", CupertinoIcons.envelope_fill, ), _TabInfo( "Option", CupertinoIcons.person_fill, ), ]; List<Widget> tabs = [ // BottomNavBar에서 연결될 각 페이지 5개 ]; @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return DefaultTextStyle( // 앱 전체에 기본 적용될 TextStyle. style: CupertinoTheme.of(context).textTheme.textStyle, child: CupertinoTabScaffold( restorationId: 'cupertino_tab_scaffold', tabBar: CupertinoTabBar( items: [ for (int i = 0; i < tabInfo.length; i++) BottomNavigationBarItem( icon: Icon(tabInfo[i].icon), backgroundColor: Colors.white, ), ], activeColor: Colors.black, iconSize: 27, ), tabBuilder: (context, index) { return CupertinoTabView( onGenerateRoute: Routes.generateRoute, builder: (ctx) { return GestureDetector( child: tabs[index], onTap: () => setState( () => _controller.index = index, ), ); }, ); }, ), ); } } class _TabInfo { const _TabInfo(this.title, this.icon); final String title; final IconData icon; }
기본 알림 뱃지(빨간 점) 띄우기
우선 위쪽 코드에서 이 부분만 떼와서 보여주면서 설명하면 될 듯 싶다.
tabBar: CupertinoTabBar( items: [ for (int i = 0; i < tabInfo.length; i++) BottomNavigationBarItem( icon: Icon(tabInfo[i].icon), backgroundColor: Colors.white, ), ], activeColor: Colors.black, iconSize: 27, ),
일단 기본적으로는, Stack을 이용해서 Icon 위에 그대로 덮는 느낌이다.
다른 모양의 Icon에 적용하고 싶으면 Positioned의 right, top 을 입맛에 맞게 조정~
tabBar: CupertinoTabBar( items: [ for (int i = 0; i < tabInfo.length; i++) BottomNavigationBarItem( icon: Stack( children: [ Icon(tabInfo[i].icon), if (i == 3) Positioned( top: 0, right: -3, child: Container( width: 10, height: 10, decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), ), ) ], ), backgroundColor: Colors.white, ), ], activeColor: Colors.black, iconSize: 27, ),
이런식으로 구현을 하면, if(i == 3)
으로 조건을 걸어줬으니, 4번째 아이콘에만 빨간 점이 나온다.

그런데, 오른쪽이 잘려 보인다.
조금 만지다 보니, 대충 BottomNavigationBarItem이 차지하는 크기가.. 핸드폰의 전체 가로 너비를 5등분해서 가져가는 게 아니라, Icon
속성의 크기를 따라가는 것 같았다.
그래서 적당히 Icon에 padding 넣어줘서 해결했다.
물론 늘려준 padding에 따라 Positioned의 top, right값은 조절.
BottomNavigationBarItem( icon: Stack( children: [ Padding( padding: EdgeInsets.all(10), child: Icon(tabInfo[i].icon), ), if (i == 3) Positioned( top: 10, right: 7, child: Container( width: 10, height: 10, decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), ), ) ], ), backgroundColor: Colors.white, ),
이러면 이제 그럴싸하게 완성됨.

숫자가 들어간 알림 뱃지 띄우기
얘도 크게 다를 바는 없다.
빨간 점 띄워주는 부분만 숫자로 바꿔주면 된다.
TextStyle whiteTextStyle_Bold(double size_) { return TextStyle( fontFamily: 'GmarketSans', color: Colors.white, fontSize: size_, fontWeight: FontWeight.bold, ); } //----- int count = 3; //----- BottomNavigationBarItem( icon: Stack( children: [ Padding( padding: EdgeInsets.all(10), child: Icon(tabInfo[i].icon), ), if (i == 3) Positioned( top: 3, right: 3, child: Container( padding: EdgeInsets.all(5), decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), child: Text("$count", style: whiteTextStyle_Bold(11), textAlign: TextAlign.center), ), ) ], ), backgroundColor: Colors.white, ),

근데 이제 숫자가 두자리 수 까진 괜찮은데, 3자리수로 가게 되면 안 예쁘게 나온다.

그래서 3자리수(100 이상일 때)는 원 안에 숫자가 아니라 카톡 알림 배지처럼. 동글동글하게 띄워보자.
BottomNavigationBarItem( icon: Stack( children: [ Padding( padding: EdgeInsets.all(10), child: Icon(tabInfo[i].icon), ), if (i == 3) Positioned( top: 3, right: 3, child: Container( padding: EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.red, shape: (count > 99) ? BoxShape.rectangle : BoxShape.circle, borderRadius: (count > 99) ? BorderRadius.circular(50) : null, ), child: Text( "$count", style: whiteTextStyle_Bold(11), textAlign: TextAlign.center, ), ), ) ], ), backgroundColor: Colors.white, ),

굿. 깔끔하다.
디테일 조정은 원하는대로 입맛에 맞게 하세용.
최종 코드
이제, 만든 뱃지 부분도 함수로 깔끔하게 빼버리고, 999개 이상일 때는 999+로 바꿔 주고, 0개일 때는 뱃지 없애 주는 등 조건도 좀 넣어주고 깔끔하게 코드 정리해 보면~
class Homescreen extends StatefulWidget { @override _HomescreenState createState() => _HomescreenState(); } class _HomescreenState extends State<Homescreen> { CupertinoTabController _controller = CupertinoTabController(); @override void dispose() { _controller.dispose(); super.dispose(); } final tabInfo = [ _TabInfo( "Home", CupertinoIcons.house_fill, ), _TabInfo( "Friends", CupertinoIcons.person_2_fill, ), _TabInfo( "Chat", CupertinoIcons.ellipses_bubble_fill, ), _TabInfo( "Message", CupertinoIcons.envelope_fill, ), _TabInfo( "Option", CupertinoIcons.person_fill, ), ]; List<Widget> tabs = [ // 연결될 페이지 5개 ]; @override void initState() { super.initState(); } @override Widget build(BuildContext context) { int count = 3; return DefaultTextStyle( // 앱 전체에 적용될 기본 teststyle. 필요없으면 이 부분 벗겨내고 사용. style: CupertinoTheme.of(context).textTheme.textStyle, child: CupertinoTabScaffold( restorationId: 'cupertino_tab_scaffold', tabBar: CupertinoTabBar( items: [ for (int i = 0; i < tabInfo.length; i++) BottomNavigationBarItem( icon: Stack( children: [ Padding( padding: EdgeInsets.all(10), child: Icon(tabInfo[i].icon), ), if (i == 3) alertBadge(count) ], ), backgroundColor: Colors.white, ), ], activeColor: Colors.black, iconSize: 27, ), tabBuilder: (context, index) { return CupertinoTabView( onGenerateRoute: Routes.generateRoute, builder: (ctx) { return GestureDetector( child: tabs[index], onTap: () => setState( () => _controller.index = index, ), ); }, ); }, ), ); } Widget alertBadge(int count) { if (count == 0) return SizedBox.shrink(); return Positioned( top: 3, right: 3, child: Container( padding: EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.red, shape: (count > 99) ? BoxShape.rectangle : BoxShape.circle, borderRadius: (count > 99) ? BorderRadius.circular(50) : null, ), child: Text( (count > 999) ? "999+" : "$count", style: whiteTextStyle_Bold(11), textAlign: TextAlign.center, ), ), ); } } class _TabInfo { const _TabInfo(this.title, this.icon); final String title; final IconData icon; } TextStyle whiteTextStyle_Bold(double size_) { return TextStyle( fontFamily: 'GmarketSans', color: Colors.white, fontSize: size_, fontWeight: FontWeight.bold, ); }