I'm using Beamer to implement a tabbed interface with a navigation stack on each tab; similar to one of the Beamer examples.
However, I'm driving the list of tabs from a Riverpod provider, rather than hard-coding them, and also monitoring the tab selection from another provider.
What I'm finding is that when I hot reload, I get an error thrown:
Multiple widgets used the same GlobalKey.
The key [GlobalKey#e2e03 beamer-/hq] was used by multiple widgets. The parents of those widgets were:
- IndexedStack(alignment: AlignmentDirectional.topStart, fit: loose, dependencies: [Directionality], renderObject: RenderIndexedStack#51cdd relayoutBoundary=up1 NEEDS-PAINT)
- IndexedStack(alignment: AlignmentDirectional.topStart, fit: loose, dependencies: [Directionality], renderObject: RenderIndexedStack#090dd relayoutBoundary=up1)
A GlobalKey can only be specified on one widget at a time in the widget tree."
The problem seems to stem from the IndexedStack that sits at the root of the tab bar, and which contains Beamers. I have cached the Beamers and their delegates, so they are only created once.
When the build() method runs for the tab bar, a new IndexedStack is returned, containing the cached Beamers. At this point I would have expected the previous IndexedStack to have been disposed, but it seems that this is not happening. The consequence is the error being thrown, which is complaining that both IndexedStack instances contain the same Beamers with the same keys.
I'm not clear what I am doing wrong here. I think I need to cache the Beamers and/or their delegates, otherwise state will not be preserved.
It feels like some sort of race condition, or that the old IndexedStack is hanging around too long (maybe some sort of retain cycle).
Any suggestions or advice on debugging this would be welcomed!
Here's the code which makes the tab bar:
Map<String, Beamer> beamerCache = {};
Map<String, BeamerDelegate> delegateCache = {};
/// A widget class that shows the BottomNavigationBar and performs navigation
/// between tabs
class ScaffoldWithBottomNavBar extends ConsumerWidget {
const ScaffoldWithBottomNavBar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final routes = ref.watch(routeProvider);
final selectedTab = ref.watch(tabSelectionProvider).selectedTab;
final tabs = routes.tabs;
final theme = Theme.of(context);
final delegates = [for (var tab in tabs) makeDelegate(tab)];
return Scaffold(
body: IndexedStack(
index: selectedTab,
children: [
for (var delegate in delegates)
makeBeamer(delegate.initialPath, delegate)
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: selectedTab,
items: [
for (var tab in tabs)
BottomNavigationBarItem(label: tab.label, icon: Icon(tab.icon))
],
selectedItemColor: theme.toggleableActiveColor,
unselectedItemColor: theme.primaryColorDark,
onTap: (index) {
if (index != selectedTab) {
ref.read(tabSelectionProvider.notifier).setTab(index);
delegates[index].update(rebuild: false);
}
},
),
);
}
BeamerDelegate makeDelegate(TabInfo tab) {
var delegate = delegateCache[tab.path];
if (delegate != null) {
return delegate;
}
delegate = BeamerDelegate(
initialPath: tab.path,
locationBuilder: (routeInformation, parameters) {
if (routeInformation.location!.contains(tab.path)) {
return TabLocation(routeInformation, tab);
}
return NotFound(path: routeInformation.location!);
});
delegateCache[tab.path] = delegate;
return delegate;
}
Beamer makeBeamer(String path, BeamerDelegate delegate) {
var beamer = beamerCache[path];
if (beamer != null) {
return beamer;
}
beamer = Beamer(
key: GlobalKey(debugLabel: "beamer-$path"),
routerDelegate: delegate,
);
beamerCache[path] = beamer;
return beamer;
}
}
The error is caused by this line
delegates[index].update(rebuild: false);
I had the same issue, which only manifested itself during hot-reload. What this .update(rebuild: false)
call does is it forces locationBuilder
to run.
But we don't have to do this. Since we are subscribed to provider updates through .watch(tabSelectionProvider)
, the Scaffold widget will properly rebuild itself, once we set the new tab index through .setTab(index)
.
Once I removed delegates[index].update(rebuild: false);
, the problem disappeared.