Flutter: I've got a persitent custom overlay with a navigator inside. How do I show/hide the overlay based on the navigated page?

Viewed 440

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.

0 Answers
Related