flutterauto-route

How to rebuild widget tree, swapping the wrapper of a route path


I am building a responsive app, using auto_route.

The application is a simple master/detail view. My requirement is that when in desktop mode, the master (list) will be displayed on the left, and the detail on the right, in a "split screen" style.

When in mobile mode, the application will display the master and list as sequential views, pushed and popped onto a navigator (i.e, on screen one at a time).

I achieve this with the folowing: When in "desktop" mode, it will wrap the route with a "split screen" page. The split screen page has the list hard coded, and a child AutoRouter on the right.

When in "mobile" mode, it will wrap the route with a "stack screen" page. The stack screen is just a simple stack.

Now, I need to somehow restore the state between mobile and desktop modes, such that the path is retained.

i.e, if in Desktop mode, and your path is /detail/:5, you will see the split screen, list on the left, detail "5" on the right. Then you resize into mobile mode (this would happen when rotating a tablet for example) - your path remains the same, so the application switches you to the stack view, and restores a backstack with the "master list", and the "detail 5" pushed on top.

Ideas?


Solution

  • The author of AutoRoute has kindly offered a clean implementation which I have moved ahead with, and verified it works. (Thank you Milad)

    Details and a short demo video are here: https://github.com/Milad-Akarie/auto_route_library/discussions/1667

    For the sake of SO history, here's a copy of the code

    import 'package:auto_route/auto_route.dart';
    import 'package:flutter/material.dart';
    
    import 'demo_router.gr.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      MyApp({super.key});
    
      final router = DemoRouter();
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp.router(
          routerConfig: router.config(),
        );
      }
    }
    
    @AutoRouterConfig()
    class DemoRouter extends $DemoRouter {
      @override
      List<AutoRoute> get routes => [
            AutoRoute(
              page: MasterRoute.page,
              initial: true,
              children: [
                AutoRoute(page: DetailsRoute.page, path: 'details'),
                AutoRoute(page: SubDetailsRoute.page, path: 'sub-details'),
              ],
            ),
          ];
    }
    
    @RoutePage()
    class MasterScreen extends StatelessWidget {
      const MasterScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        final screenWidth = MediaQuery.sizeOf(context).width;
        final splitMode = screenWidth > 600;
        final splitPosition = splitMode ? screenWidth / 2 : 0.0;
        return AutoRouter(
          builder: (context, subRouterWidget) {
            final subRouter = context.router;
            // in split mode we show the back button only if the sub router can pop (entries.length > 1)
            final showBackButton = splitMode ? subRouter.canPop() : subRouter.hasEntries;
            return Scaffold(
              appBar: AppBar(
                /// we use router.popForced to always pop even if we have one entry in the stack
                leading: showBackButton ? BackButton(onPressed: subRouter.popForced) : null,
              ),
              body: Stack(
                children: [
                  Positioned.fill(
                    right: splitPosition,
                    child: MasterScreenContent(),
                  ),
                  Positioned.fill(
                    left: splitPosition,
                    child: AnimatedSwitcher(
                      duration: Duration(milliseconds: 200),
                      transitionBuilder: (child, animation) {
                        /// mimic route transition
                        return SlideTransition(
                          position: Tween<Offset>(
                            begin: Offset(1, 0),
                            end: Offset.zero,
                          ).animate(animation),
                          child: child,
                        );
                      },
                      child: subRouter.hasEntries ? subRouterWidget : SizedBox(),
                    ),
                  ),
                ],
              ),
            );
          },
        );
      }
    }
    
    class MasterScreenContent extends StatelessWidget {
      const MasterScreenContent({super.key});
    
      @override
      Widget build(BuildContext context) {
        return ColoredBox(
          color: Colors.grey.shade300,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                child: Text('Show details'),
                onPressed: () {
                  context.navigateTo(DetailsRoute());
                },
              ),
            ],
          ),
        );
      }
    }
    
    
    @RoutePage()
    class DetailsScreen extends StatefulWidget {
      const DetailsScreen({super.key});
    
      @override
      State<DetailsScreen> createState() => _DetailsScreenState();
    }
    
    class _DetailsScreenState extends State<DetailsScreen> {
      int _counter = 0;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.green,
          body: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text('Details'),
                Text('$_counter'),
                SizedBox(height: 10),
                ElevatedButton(
                  child: Text('Increment'),
                  onPressed: () {
                    setState(() {
                      _counter++;
                    });
                  },
                ),
                SizedBox(height: 10),
                ElevatedButton(
                  child: Text('Sub Details'),
                  onPressed: () {
                    context.pushRoute(SubDetailsRoute());
                  },
                ),
              ],
            ),
          ),
        );
      }
    }
    
    @RoutePage()
    class SubDetailsScreen extends StatelessWidget {
      const SubDetailsScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.blue,
          body: Center(
            child: Text('Sub Details'),
          ),
        );
      }
    }