0

I have an app with two features, that have routes such as:

/feature1
/feature1/a
/feature2
/feature2/a
/feature2/a/b
/feature2/c

I can use GoRouter and its ShellRoute to switch between these one at a time using context.goNamed('feature2'), which would replace the entire screen with feature 2 (when tapping a tab in a tab bar for example). Here's a diagram of just the top level routes using tabs:

Feature 1's page with a tab bar at the bottom Feature 2's page with a tab bar at the bottom

However, I would like to have an overview style menu which displays multiple destinations at once, so the user can see where they will be going before they go there (for example the preview page tabs in a mobile web browser). Here's a diagram:

Menu displaying a preview of the pages for Feature 1 and Feature 2

and then tapping on either of the two pages would make them full screen:

Feature 1's page with a menu button a the bottom Feature 2's page with a menu button a the bottom

Pressing the menu button at the bottom would return you to the overview menu page.

One way I have thought about solving this would be to make static preview images out of the routes when the menu button is tapped, and just display the previews. But these won't be live, and I would like a more elegant approach that actually displays the live contents of the route if possible.

Another way I have thought about solving this would be to use a top level GoRouter and then two descendant GoRouters each containing just one branch of the routes. I'm not sure if multiple GoRouters would lead to problems with things like if I wanted to context.go() to another branch.

If the ShellRoute.builder gave me access to all of the child page's widgets, I could display them however I wanted, but it just provides a single child.

Jaween
  • 374
  • 1
  • 3
  • 10

1 Answers1

0

I have not worked with 'go_router' or 'ShellRoute.builder', but I like to make custom animated widgets like this for apps. It's also hard to explain how it would work in your app, but here is my take on this.

Try copy pasting this in an empty page. I have written some notes in code comments that might help explain things a little bit. And, this is not perfect but with more polishing according to the needs it could work.

class CustomPageView extends StatefulWidget {
  const CustomPageView({Key? key}) : super(key: key);

  @override
  State<CustomPageView> createState() => _CustomPageViewState();
}

class _CustomPageViewState extends State<CustomPageView> {

  // Scroll Controller required to control scroll via code.
  // When user taps on the navigation buttons, we will use this controller
  // to scroll to the next/previous page.
  final ScrollController _scrollController = ScrollController();

  // Saving screen width and height to use it for the page size and page offset.
  double _screenWidth = 0;
  double _screenHeight = 0;

  // A bool to toggle between full screen mode and normal mode.
  bool _viewFull = false;

  @override
  void initState() {
    super.initState();

    // Get the screen width and height.
    // This will be used to set the page size and page offset.

    // As of now, this only works when page loads, not when orientation changes
    // or page is resized. That requires a bit more work.
    WidgetsBinding.instance.addPostFrameCallback((_) {
      setState(() {
        _screenWidth = MediaQuery.of(context).size.width;
        _screenHeight = MediaQuery.of(context).size.height;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(

      // 'Column' to wrap the 'Body' and 'BottomNavigationBar'
      body: Column(
        children: [

          // 'Expanded' to take up the remaining space after the 'BottomNavigationBar'
          Expanded(

            // A 'Container' to wrap the overall 'Body' and aligned to center.
            // So when it resizes, it will be centered.
            child: Container(
              alignment: Alignment.center,

              // 'AnimatedContainer' to animate the overall height of the 'Body'
              // when user taps on the 'Full Screen' button.
              child: AnimatedContainer(
                duration: const Duration(milliseconds: 500),
                height: _viewFull ? 200 : _screenHeight,

                // A 'ListView' to display the pages.
                // 'ListView' is used here because we want to scroll horizontally.
                // It also enables us to use 'PageView' like functionality, but
                // requires a bit more work, to make the pages snap after scrolling.
                child: ListView(
                  controller: _scrollController,
                  scrollDirection: Axis.horizontal,
                  children: [

                    // A 'Container' to display the first page.
                    AnimatedContainer(
                      duration: const Duration(milliseconds: 500),
                      width: _viewFull ? (_screenWidth / 2) - 24 : _screenWidth,
                      margin: _viewFull ? const EdgeInsets.all(12) : const EdgeInsets.all(0),
                      color: Colors.blue,
                    ),

                    // A 'Container' to display the second page.
                    AnimatedContainer(
                      duration: const Duration(milliseconds: 500),
                      width: _viewFull ? (_screenWidth / 2) - 24 : _screenWidth,
                      margin: _viewFull ? const EdgeInsets.all(12) : const EdgeInsets.all(0),
                      color: Colors.yellow,
                    ),
                  ],
                ),
              ),
            ),
          ),

          // 'BottomNavigationBar' to show the navigation buttons
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [

              // 'Feature 1' button
              GestureDetector(
                onTap: () {

                  // Scroll to the first page
                  _scrollController.animateTo(
                    0,
                    duration: const Duration(milliseconds: 500),
                    curve: Curves.easeInOut,
                  );

                },
                child: Container(
                  height: 60,
                  alignment: Alignment.center,
                  color: Colors.red,
                  padding: const EdgeInsets.all(12),
                  child: const Text('Feature 1'),
                ),
              ),

              // 'Feature 2' button
              GestureDetector(
                onTap: () {

                  // Scroll to the second page
                  _scrollController.animateTo(
                    _screenWidth,
                    duration: const Duration(milliseconds: 500),
                    curve: Curves.easeInOut,
                  );

                },
                child: Container(
                  height: 60,
                  alignment: Alignment.center,
                  color: Colors.green,
                  padding: const EdgeInsets.all(12),
                  child: const Text('Feature 2'),
                ),
              ),

              // 'Full Screen' button
              GestureDetector(
                onTap: () {

                  // Toggle between full screen mode and normal mode
                  setState(() {
                    _viewFull = !_viewFull;
                  });

                },
                child: Container(
                  height: 60,
                  alignment: Alignment.center,
                  color: Colors.purple,
                  padding: const EdgeInsets.all(12),
                  child: const Text('View Full'),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}
Akshaye JH
  • 70
  • 8
  • Thanks for your answer, but this solution loses access to Flutter's navigation functionality. The question has subroutes that this solution won't be able to handle. Though adding a Navigator to each branch here is similar to the second approach I suggested. – Jaween Jan 29 '23 at 00:42