Search code examples
flutterhero

Popping Routes in a Navigation Stack Causes Hero Widget to Disappear


Question:

I'm working on a Flutter application that uses a navigation bar to switch between different pages. Each page has its own navigation stack, managed by a Navigator with a GlobalKey. When popping routes from the navigation stack, the Hero widget disappears. However, this issue does not occur when popping from a widget within the screen itself.

Here's the relevant code:

HolupNavigationPage Widget:

My navigator widget for managing multiple navigator stacks.

class HolupNavigationPage extends StatefulWidget {
  @override
  _HolupNavigationPageState createState() => _HolupNavigationPageState();
}

class _HolupNavigationPageState extends State<HolupNavigationPage> {
  int _pageIndex = 0;
  final Map<int, GlobalKey<NavigatorState>> navigatorKeys = {
    0: GlobalKey<NavigatorState>(),
    1: GlobalKey<NavigatorState>(),
    2: GlobalKey<NavigatorState>(),
    3: GlobalKey<NavigatorState>(),
  };

  final List<Widget> baseScreens = [
    const HolupWHSearchWorkHomePage(),
    const HolupCvStats(),
    const HolupWHSearchHousingHomePage(),
    const HolupHintsPage()
  ];

      Future<void> _clearStack() async {
    if (Navigator.canPop(navigatorKeys[_pageIndex]!.currentContext!)) {
      Navigator.of(navigatorKeys[_pageIndex]!.currentContext!)
          .popUntil((route) => route.isFirst);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: WillPopScope(
          onWillPop: () async {
            return !await Navigator.maybePop(navigatorKeys[_pageIndex]!.currentState!.context);
          },
          child: IndexedStack(
            index: _pageIndex,
            children: <Widget>[
              HeroControllerScope(
                controller: MaterialApp.createMaterialHeroController(),
                child: NavigatorPage(
                  child: const HolupWHSearchWorkHomePage(),
                  navigatorKey: navigatorKeys[0]!,
                ),
              ),
              HeroControllerScope(
                controller: MaterialApp.createMaterialHeroController(),
                child: NavigatorPage(
                  child: const HolupCvStats(),
                  navigatorKey: navigatorKeys[1]!,
                ),
              ),
              HeroControllerScope(
                controller: MaterialApp.createMaterialHeroController(),
                child: NavigatorPage(
                  child: const HolupWHSearchHousingHomePage(),
                  navigatorKey: navigatorKeys[2]!,
                ),
              ),
              HeroControllerScope(
                controller: MaterialApp.createMaterialHeroController(),
                child: NavigatorPage(
                  child: const HolupHintsPage(),
                  navigatorKey: navigatorKeys[3]!,
                ),
              ),
            ],
          ),
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.work),
            label: 'Práca',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.assignment),
            label: 'Životopis',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Ubytovanie',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.tips_and_updates),
            label: 'Rady a tipy',
          ),
        ],
        currentIndex: _pageIndex,
        onTap: (int index) {
          setState(() {
            if (index == _pageIndex) {
              _clearStack();
            } else {
              _pageIndex = index;
            }
          });
        },
      ),
    );
  }
}

class NavigatorPage extends StatefulWidget {
  final Widget child;
  final GlobalKey navigatorKey;

  NavigatorPage({required this.navigatorKey, required this.child});

  @override
  _NavigatorPageState createState() => _NavigatorPageState();
}

class _NavigatorPageState extends State<NavigatorPage> {
  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: widget.navigatorKey,
      onGenerateRoute: (RouteSettings settings) {
        return MaterialPageRoute(
          settings: settings,
          builder: (BuildContext context) {
            return widget.child;
          },
        );
      },
    );
  }
}

HolupMobileHeader widget:

Widget from where inside of context is pop called, and its working properly.

class HolupMobileHeader extends StatelessWidget {
  final HolupMobileHeaderType type;
  final Widget? action;
  final Widget? title;
  final Color backgroundColor;

  const HolupMobileHeader({
    Key? key,
    this.type = HolupMobileHeaderType.none,
    this.action,
    this.title,
    this.backgroundColor = HolupColors.background,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      color: backgroundColor,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const SizedBox(height: 32),
            Row(
              mainAxisSize: MainAxisSize.max,
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                Expanded(
                  child: Align(
                    alignment: Alignment.centerLeft,
                    child: type == HolupMobileHeaderType.none
                        ? const SizedBox(height: 24)
                        : HolupIconButton(
                            icon: 'arrow-right',
                            onTap: () {
                              Navigator.of(context).pop();
                            },
                            iconSize: 16,
                            size: 40,
                            iconColor: HolupColors.white,
                            color: Colors.orange,
                            flip: true,
                          ),
                  ),
                ),
                if (title != null) Align(
                  alignment: Alignment.center,
                  child: title,
                ),
                if (type != HolupMobileHeaderType.menu) Expanded(
                  child: Align(
                    alignment: Alignment.centerRight,
                    child: action == null ? const SizedBox(width: 24) : action!,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16)
          ],
        ),
      ),
    );
  }
}

SearchAreaMobile Widget:

Widget where Hero widget is.

class SearchAreaMobile extends StatelessWidget {
  final String moduleName;
  final String? title;
  final VoidCallback? onFocus;
  final String? hint;
  final List<Widget>? quickActions;
  final Widget navChips;
  final bool showForm;

  const SearchAreaMobile({
    Key? key,
    this.showForm = true,
    required this.moduleName,
    this.title,
    this.onFocus,
    this.hint,
    this.quickActions,
    required this.navChips,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        HolupMobileHeader(type: moduleName == 'Práca' ? HolupMobileHeaderType.menu : HolupMobileHeaderType.none),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.only(left: 4, top: 16),
                child: Text(
                  moduleName,
                  style: HolupTextStyles.headlineMobile,
                ),
              ),
              Padding(padding: const EdgeInsets.only(top: 8), child: navChips),
              if (showForm) ...[
                const SizedBox(height: 32),
                Padding(
                  padding: const EdgeInsets.only(left: 4, bottom: 8),
                  child: Text(
                    title!,
                    style: HolupTextStyles.subtitleLightMobile.white,
                    overflow: TextOverflow.visible,
                  ),
                ),
                HolupLink(
                  onTap: onFocus!,
                  child: AbsorbPointer(
                    child: Hero(
                      tag: moduleName == 'Práca' ? 'search_field_work' : 'search_field_housing',
                      child: HolupSearchField(
                        icon: 'search',
                        hint: hint,
                        readOnly: true,
                      ),
                    ),
                  ),
                ),
                const SizedBox(height: 13),
                Align(
                  alignment: Alignment.center,
                  child: Wrap(
                    children: quickActions!,
                  ),
                ),
                const SizedBox(height: 48,)
              ] else ...[
                const SizedBox(height: 24)
              ]
            ],
          ),
        ),
      ],
    );
  }
}

The Issue

When navigating between different tabs using the navigation bar and popping routes, the Hero widget disappears. However, when popping from a widget inside the screen (e.g., HolupMobileHeader), it works fine.

Question Why does the Hero widget disappear when popping routes from the navigation stack using the navigation bar, and how can I ensure that the Hero widget transitions correctly in this scenario?


Solution

  • Try to define your HeroControllers outside of the build method and pass it to your IndexedStack, so it doesn't get rebuild.

    List _pages = [HeroControllerScope(
                controller: MaterialApp.createMaterialHeroController(),
                child: NavigatorPage(
                  child: const HolupWHSearchWorkHomePage(),
                  navigatorKey: navigatorKeys[0]!,
                ),
              ),
              HeroControllerScope(
                controller: MaterialApp.createMaterialHeroController(),
                child: NavigatorPage(
                  child: const HolupCvStats(),
                  navigatorKey: navigatorKeys[1]!,
                ),
              ),
              HeroControllerScope(
                controller: MaterialApp.createMaterialHeroController(),
                child: NavigatorPage(
                  child: const HolupWHSearchHousingHomePage(),
                  navigatorKey: navigatorKeys[2]!,
                ),
              ),
              HeroControllerScope(
                controller: MaterialApp.createMaterialHeroController(),
                child: NavigatorPage(
                  child: const HolupHintsPage(),
                  navigatorKey: navigatorKeys[3]!,
                ),
              )];
    

    Your IndexedStack gets the list:

    IndexedStack(
            index: _pageIndex,
            children: _pages,
    

    Since we are outside the build method now, the controllers shouldn't rebuild if you pop from the Navigation Bar.