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?
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.