I'm building a Flutter app with a custom bottom navigation bar using Rive animations. Each navigation item has a Rive animation that plays on tap, and pages are switched using IndexedStack.
Problem: Tapping on the Dashboard button animates the Notification icon instead.
Tapping on the Notification button triggers logic but animates the Profile icon.
The Profile button does not register taps at all.
Oddly, if I hot-reload the app (e.g. via Ctrl + S in VS Code), the navigation bar starts behaving correctly until the next restart.
Home_content.dart
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> with TickerProviderStateMixin {
final List<Widget> allPages = const [
HomeContentBody(),
SearchPage(),
DashboardPage(),
NotificationPage(),
ProfilePage(),
];
final List<SMIBool> riveIconInputs = [];
final List<StateMachineController?> controllers = [];
int selectedNavIndex = 0;
int? hoveredIndex;
int notificationCount = Counter.count;
bool inSession = true;
bool isFabExpanded = false;
Offset fabPosition = const Offset(30, 100);
late AnimationController fabAnimationController;
final List<Map<String, dynamic>> fabActions = [
{
'icon': Icons.call,
'label': 'Call Support',
'color': Color.fromRGBO(46, 125, 50, 1),
},
{
'icon': Icons.chat,
'label': 'Chat Support',
'color': Color.fromRGBO(46, 125, 50, 1),
},
{
'icon': Icons.mail,
'label': 'Mail Support',
'color': Color.fromRGBO(46, 125, 50, 1),
},
];
List<bool> visibleLabels = [];
@override
void initState() {
super.initState();
fabAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
visibleLabels = List.filled(fabActions.length, false);
}
@override
void dispose() {
for (var controller in controllers) {
controller?.dispose();
}
fabAnimationController.dispose();
super.dispose();
}
void animateTheIcon(int index) {
if (riveIconInputs.length > index) {
riveIconInputs[index].change(true);
setState(() {
selectedNavIndex = index;
if (index == 3) notificationCount = 0;
});
Future.delayed(const Duration(seconds: 1), () {
riveIconInputs[index].change(false);
});
}
}
void riveOnInit(
Artboard artboard, {
required String stateMachineName,
required int index,
}) {
if (riveIconInputs.length > index) return;
final controller = StateMachineController.fromArtboard(
artboard,
stateMachineName,
);
if (controller == null) return;
artboard.addController(controller);
final input = controller.findInput<bool>('active') as SMIBool?;
if (input == null) return;
input.value = false;
riveIconInputs.add(input);
controllers.add(controller);
}
void closeFabMenu() {
setState(() => visibleLabels = List.filled(fabActions.length, false));
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) setState(() => isFabExpanded = false);
});
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final screenWidth = screenSize.width;
final screenHeight = screenSize.height;
final visibleIndices =
List.generate(
bottomNavItems.length,
(i) => i,
).where((i) => inSession || i < 3).toList();
if (!visibleIndices.contains(selectedNavIndex)) {
selectedNavIndex = visibleIndices.first;
}
final currentIndex = visibleIndices.indexOf(selectedNavIndex);
return Stack(
children: [
Scaffold(
body: IndexedStack(
index: currentIndex,
children: visibleIndices.map((i) => allPages[i]).toList(),
),
bottomNavigationBar: BottomNavBar(
visibleIndices: visibleIndices,
selectedNavIndex: selectedNavIndex,
hoveredIndex: hoveredIndex,
notificationCount: notificationCount,
animateTheIcon: animateTheIcon,
riveOnInit: riveOnInit,
onHoverChange: (i) => setState(() => hoveredIndex = i),
),
),
if (isFabExpanded)
GestureDetector(
onTap: closeFabMenu,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(color: Colors.black.withValues(alpha: 0.3)),
),
),
FabMenu(
isExpanded: isFabExpanded,
actions: fabActions,
fabPosition: fabPosition,
visibleLabels: visibleLabels,
onToggle: () {
setState(() {
isFabExpanded = !isFabExpanded;
visibleLabels = List.filled(fabActions.length, isFabExpanded);
});
if (isFabExpanded) {
Future.delayed(const Duration(seconds: 2), () {
if (mounted && isFabExpanded) {
setState(
() => visibleLabels = List.filled(fabActions.length, false),
);
}
});
}
},
onDragUpdate: (offset) {
setState(() {
const fabSize = 56.0;
const padding = 10.0;
fabPosition = Offset(
(fabPosition.dx - offset.delta.dx).clamp(
padding,
screenWidth - fabSize - padding,
),
(fabPosition.dy - offset.delta.dy).clamp(
padding,
screenHeight - fabSize - padding,
),
);
});
},
onActionTap: (label) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$label tapped'),
duration: const Duration(seconds: 1),
),
);
closeFabMenu();
},
),
],
);
}
}
Bottom_nav_bar.dart
class BottomNavBar extends StatelessWidget {
const BottomNavBar({
super.key,
required this.visibleIndices,
required this.selectedNavIndex,
required this.hoveredIndex,
required this.notificationCount,
required this.animateTheIcon,
required this.riveOnInit,
required this.onHoverChange,
});
final List<int> visibleIndices;
final int selectedNavIndex;
final int? hoveredIndex;
final int notificationCount;
final void Function(int) animateTheIcon;
final void Function(
Artboard, {
required String stateMachineName,
required int index,
})
riveOnInit;
final void Function(int?) onHoverChange;
String getTooltipMessage(int index) {
const tooltips = [
'Home',
'Search',
'Dashboard',
'Notifications',
'Profile',
];
return tooltips[index];
}
@override
Widget build(BuildContext context) {
final isSmallScreen = MediaQuery.of(context).size.width < 400;
return SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
return Container(
padding: EdgeInsets.symmetric(
horizontal: width * 0.05,
vertical: 5,
),
margin: EdgeInsets.only(
left: width * 0.06,
right: width * 0.06,
bottom: 15,
),
decoration: BoxDecoration(
color: const Color.fromARGB(255, 7, 35, 9).withAlpha(230),
borderRadius: BorderRadius.circular(40),
boxShadow: const [
BoxShadow(
color: Color.fromRGBO(14, 30, 37, 0.12),
blurRadius: 4,
offset: Offset(0, 2),
),
BoxShadow(
color: Color.fromRGBO(14, 30, 37, 0.32),
blurRadius: 16,
offset: Offset(0, 2),
),
],
),
child: Row(
children:
bottomNavItems.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
if (!visibleIndices.contains(index)){
return const SizedBox();
}
final isActive = selectedNavIndex == index;
return Expanded(
child: MouseRegion(
onEnter: (_) => onHoverChange(index),
onExit: (_) => onHoverChange(null),
child: Tooltip(
message: getTooltipMessage(index),
child: GestureDetector(
onTap:
() =>
animateTheIcon(index), // Send global index
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
color:
hoveredIndex == index && !isActive
? Colors.white.withAlpha(13)
: Colors.transparent,
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedBar(isActive: isActive),
Stack(
children: [
SizedBox(
height: isSmallScreen ? 24 : 30,
width: isSmallScreen ? 24 : 30,
child: Opacity(
opacity: isActive ? 1 : 0.5,
child: RiveAnimation.asset(
item.rive.src,
artboard: item.rive.artboard,
onInit:
(artboard) => riveOnInit(
artboard,
stateMachineName:
item
.rive
.stateMachineName,
index:
index, // Use global index
),
),
),
),
if (index == 3 && notificationCount > 0)
Positioned(
top: -2,
right: -2,
child: Container(
padding: const EdgeInsets.all(3),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
'${notificationCount > 99 ? '99+' : notificationCount}',
style: const TextStyle(
color: Color.fromRGBO(
46,
125,
50,
1,
),
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
),
],
),
),
),
),
),
);
}).toList(),
),
);
},
),
);
}
}
nav_item_model.dart
import 'rive_model.dart';
class NavItemModel {
final String title;
final RiveModel rive;
NavItemModel({
required this.title,
required this.rive
});
}
List<NavItemModel>bottomNavItems = [
NavItemModel(
title: "Home",
rive: RiveModel(
src:"assets/navicons.riv",
artboard:"HOME",
stateMachineName: "HOME_interactivity"),
),
NavItemModel(
title: "Search",
rive: RiveModel(
src: "assets/navicons.riv",
artboard: "SEARCH",
stateMachineName: "SEARCH_Interactivity"),
),
NavItemModel(
title: "Dashboard",
rive: RiveModel(
src: "assets/navicons2.riv",
artboard: "DASH",
stateMachineName: "DASH_Interactivity"),
),
NavItemModel(
title: "Notification",
rive: RiveModel(
src: "assets/navicons.riv",
artboard: "BELL",
stateMachineName: "BELL_Interactivity"),
),
NavItemModel(
title: "Profile",
rive: RiveModel(
src: "assets/navicons.riv",
artboard: "USER",
stateMachineName: "USER_Interactivity"),
),
];
animated_bar.dart
import 'package:flutter/material.dart';
class AnimatedBar extends StatelessWidget {
const AnimatedBar({super.key, required this.isActive});
final bool isActive;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 4),
height: 4,
width: isActive ? 25 : 0,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
);
}
}