I want to implement an overlay that uses the auto_route package to navigate between pages inside it. I want the overlay to persist across pages, so if the search bar or the drawer are open, once you push or pop a page its aspect persists.
The problem comes with the next requirement: I want it to be hidden or shown based on the page that is going to be navigated to. That is, if I am in "Page1" where the overlay is shown and I do "Navigator.pushNamed(/Page2)" and Page2 has the overlay hidden, the overlay hides. And if I do "Navigator.pop()" it goes back to "Page1", the overlay is shown again.
Because the page is given by an "ExtendedNavigator" class and the Overlay itself is built first, I cannot know in build time where the Navigator is navigating to or which page is currently being built. I need to know this information beforehand to hide or show the Overlay accordingly.
I tried several solutions with no avail.
First approach
Instantiating the Overlay in every page that needs it to be rendered, and using an Inherited Widget to maintain the state.
Problem: Cannot keep the state of all the overlays syncronized. Keys cannot be re-used, nor their state be copied (for ScaffoldState, for example). So any time there is a change in one of the overlays, I should replicate the same change to all the other overlays to change their state accordingly. Also, cannot instantiate a Scaffold with a specific State.
The code for this solution looked approximately like this:
class Page1 extends StatefulWidget {
Page1 ({
Key key,
}) : super(key: key);
@override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
@override
Widget build(BuildContext context) {
return Overlay(
child: (...)
);
}
}
class Page2 extends StatefulWidget {
Page2 ({
Key key,
}) : super(key: key);
@override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2> {
@override
Widget build(BuildContext context) {
return Overlay(
child: (...)
);
}
}
class OverlayData extends InheritedWidget {
bool _hidden = true;
OverlayData({
Key key,
@required Widget child,
}) : super(key: key, child: child);
static OverlayData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<OverlayData>();
}
void setHiddenTo(bool value) {
_hidden = value;
print("Overlay is now in state: " + value.toString());
}
bool isHidden() {
return _hidden;
}
@override
bool updateShouldNotify(OverlayData oldWidget) =>
_hidden != oldWidget._hidden;
}
//// Main
void main() {
runApp(App());
}
class App extends StatefulWidget {
@override
State<StatefulWidget> createState() => _AppState();
}
class _AppState extends State<App> {
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: ExtendedNavigator.builder<router.Router>(
router: router.Router(),
builder: (context, extendedNav) => Theme(
data: ThemeData(
brightness: Brightness.light,
),
child: OverlayData(child: Overlay(child: extendedNav)),
),
),
color: StyleValues.blue,
);
}
}
Second approach
Everytime there is a manual push/pop to another page, manually trigger the hiding/showing of the overlay with its GlobalKey. When pushing a page I can use [Guards] to check if the page that I'm navigating to is one with the overlay shown or hidden, and trigger the change with the Key of the Overlay.
Problem: When I pop using the Android Button, I can not know to which page I am popping back and trigger the change accordingly. Even if I wrap my overlay with a "WillPopScope", I still don't know to which page I am popping, so there is no use in it.
The code for this looked approximately like this:
/// Main
void main() {
runApp(App());
}
class App extends StatefulWidget {
@override
State<StatefulWidget> createState() => _AppState();
}
class _AppState extends State<App> {
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: ExtendedNavigator.builder<router.Router>(
router: router.Router(),
guards: [HideGuard(), ShowGuard()]
builder: (context, extendedNav) => Theme(
data: ThemeData(
brightness: Brightness.light,
),
child: Overlay(child: extendedNav),
),
),
color: StyleValues.blue,
);
}
}
/// Overlay
class Overlay extends StatefulWidget {
final Widget body;
Overlay({this.body});
@override
OverlayState createState() => OverlayState(this.body);
}
class OverlayState extends State<Overlay> {
final Widget body;
bool _hidden;
OverlayState(this.body);
void setHiddenTo(bool value){
if (value != _hidden)
{
SetState(() {
_hidden = value;
})
}
}
@override
Widget build(BuildContext context) {
/// Here I check if it is hidden or not
if (_hidden) {
return body;
}
return Scaffold(
appBar: PreferredSize(
child: AppBar(...)
),
/// Bottom Bar
bottomNavigationBar: (...),
/// Content (Second Scaffold to render Drawer behind AppBar and BottomBar)
body: Scaffold(
key: _scaffoldKey,
drawer: Drawer(
child: (...)
),
body: body,
),
);
}
}
/// The router
@MaterialAutoRouter(routes: <AutoRoute>[
MaterialRoute(path: PageRoutes.page1, page: Page1, initial: true, guards: [HideGuard]),
MaterialRoute(path: PageRoutes.page2, page: Page2, guards: [ShowGuard]),
MaterialRoute(path: PageRoutes.page3, page: Page3, guards: [ShowGuard]),
])
class $Router {}
/// The Key (in a separate class)
class Keys{
GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
}
/// A guard
class ShowGuard extends RouteGuard {
@override
Future<bool> canNavigate(
ExtendedNavigatorState navigator, String routeName, Object arguments) async {
Keys.OverlayKey.currentState.setHiddenTo(false);
}
}
class HideGuard extends RouteGuard {
@override
Future<bool> canNavigate(
ExtendedNavigatorState navigator, String routeName, Object arguments) async {
Keys.OverlayKey.currentState.setHiddenTo(true);
}
}
How should I approach the problem? Is there any way to retrieve the current page that is going to be pushed/pop before the overlay is built? Is there any way for several widgets to share the same state? Thank you in advance for the help.