1

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),
              ),
            );
          }
        }

1
  • Please trim your code to make it easier to find your problem. Follow these guidelines to create a minimal reproducible example. Commented Jun 17 at 22:59

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.