lulupointu / vrouter

MIT License
202 stars 38 forks source link

Stacked routes inside of nested routes #213

Closed oravecz closed 1 year ago

oravecz commented 1 year ago

I have a deep level of nested routes, including wrapping widgets that provide headers and nav bar, but also sometimes BlocProviders.

I am currently declaring my stacked routes as children of the nested routes, and those stacked pages are displaying within the child areas of the nested routes.

I want the stacked routes to visually cover the headers and nav bars, not be displayed within the child areas of the nested route.

Do I need to declare these routes at the root (peer with my nested routes) for them to cover the nested routes?

Or, do I achieve this using specific Keys?

lulupointu commented 1 year ago

You can use VNester.stackedRoutes. Would that work for your use case?

oravecz commented 1 year ago

I took your example and added a stackedRoutes property to the Settings route.

My intent is to learn how I can get the SlideOverWidget to fill the viewport, even though it is a stacked route inside the Settings route which, in turn, is a nested route inside MyScaffold.

import 'package:flutter/material.dart';
import 'package:vrouter/vrouter.dart';

void main() {
  runApp(
    VRouter(
      debugShowCheckedModeBanner: false,
      // VRouter acts as a MaterialApp
      mode: VRouterMode.history,
      // Remove the '#' from the url
      // logs: [VLogLevel.info], // Defines which logs to show, info is the default
      routes: [
        VWidget(
          path: '/login',
          widget: LoginWidget(),
          stackedRoutes: [
            ConnectedRoutes(), // Custom VRouteElement
          ],
        ),
        // This redirect every unknown routes to /login
        VRouteRedirector(
          redirectTo: '/login',
          path: r'*',
        ),
      ],
    ),
  );
}

// Extend VRouteElementBuilder to create your own VRouteElement
class ConnectedRoutes extends VRouteElementBuilder {
  static final String profile = 'profile';

  static void toProfile(BuildContext context, String username) =>
      context.vRouter.to('/$username/$profile');

  static final String settings = 'settings';
  static final String slideOver = 'slideover';

  static void toSettings(BuildContext context, String username) =>
      context.vRouter.to('/$username/$settings');

  @override
  List<VRouteElement> buildRoutes() {
    return [
      VNester.builder(
        // .builder constructor gives you easy access to VRouter data
        path:
            '/:username', // :username is a path parameter and can be any value
        widgetBuilder: (_, state, child) => MyScaffold(
          child,
          currentIndex: state.names.contains(profile) ? 0 : 1,
        ),
        nestedRoutes: [
          VWidget(
            path: profile,
            name: profile,
            widget: ProfileWidget(),
          ),
          VWidget(
            path: settings,
            name: settings,
            widget: SettingsWidget(),

            // Custom transition
            buildTransition: (animation, ___, child) {
              return ScaleTransition(
                scale: animation,
                child: child,
              );
            },
            stackedRoutes: [
              VWidget(
                name: slideOver,
                path: slideOver,
                widget: SlideOverWidget(),
              )
            ],
          ),
        ],
      ),
    ];
  }
}

class LoginWidget extends StatefulWidget {
  @override
  _LoginWidgetState createState() => _LoginWidgetState();
}

class _LoginWidgetState extends State<LoginWidget> {
  String name = 'bob';
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Row(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Text('Enter your name to connect: '),
                Expanded(
                  child: Container(
                    decoration: BoxDecoration(
                      border: Border.all(color: Colors.black),
                    ),
                    child: Form(
                      key: _formKey,
                      child: TextFormField(
                        textAlign: TextAlign.center,
                        onChanged: (value) => name = value,
                        initialValue: 'bob',
                      ),
                    ),
                  ),
                ),
              ],
            ),
            // This FAB is shared and shows hero animations working with no issues
            FloatingActionButton(
              heroTag: 'FAB',
              onPressed: () {
                setState(() => (_formKey.currentState!.validate())
                    ? ConnectedRoutes.toProfile(context, name)
                    : null);
              },
              child: Icon(Icons.login),
            )
          ],
        ),
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  final Widget child;
  final int currentIndex;

  const MyScaffold(this.child, {required this.currentIndex});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('You are connected'),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: currentIndex,
        items: [
          BottomNavigationBarItem(
              icon: Icon(Icons.person_outline), label: 'Profile'),
          BottomNavigationBarItem(
              icon: Icon(Icons.info_outline), label: 'Info'),
        ],
        onTap: (int index) {
          // We can access this username via the path parameters
          final username = VRouter.of(context).pathParameters['username']!;
          if (index == 0) {
            ConnectedRoutes.toProfile(context, username);
          } else {
            ConnectedRoutes.toSettings(context, username);
          }
        },
      ),
      body: child,

      // This FAB is shared with login and shows hero animations working with no issues
      floatingActionButton: FloatingActionButton(
        heroTag: 'FAB',
        onPressed: () => VRouter.of(context).to('/login'),
        child: Icon(Icons.logout),
      ),
    );
  }
}

class ProfileWidget extends StatefulWidget {
  @override
  _ProfileWidgetState createState() => _ProfileWidgetState();
}

class _ProfileWidgetState extends State<ProfileWidget> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    // VNavigationGuard allows you to react to navigation events locally
    return VWidgetGuard(
      // When entering or updating the route, we try to get the count from the local history state
      // This history state will be NOT null if the user presses the back button for example
      afterEnter: (context, __, ___) => getCountFromState(context),
      afterUpdate: (context, __, ___) => getCountFromState(context),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 20.0),
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextButton(
                onPressed: () {
                  VRouter.of(context).to(
                    context.vRouter.url,
                    isReplacement: true,
                    historyState: {'count': '${count + 1}'},
                  );
                  setState(() => count++);
                },
                child: Container(
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(50),
                    color: Colors.blueAccent,
                  ),
                  padding:
                      EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0),
                  child: Text(
                    'Your pressed this button $count times',
                    style: buttonTextStyle,
                  ),
                ),
              ),
              SizedBox(height: 20),
              Text(
                'This number is saved in the history state so if you are on the web leave this page and hit the back button to see this number restored!',
                style: textStyle,
                textAlign: TextAlign.center,
              ),
            ],
          ),
        ),
      ),
    );
  }

  void getCountFromState(BuildContext context) {
    setState(() {
      count = (VRouter.of(context).historyState['count'] == null)
          ? 0
          : int.tryParse(VRouter.of(context).historyState['count'] ?? '') ?? 0;
    });
  }
}

class SettingsWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 20.0),
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              'Did you see the custom animation when coming here?',
              style: textStyle.copyWith(fontSize: textStyle.fontSize! + 2),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                final username =
                    VRouter.of(context).pathParameters['username']!;
                context.vRouter.toNamed(
                  ConnectedRoutes.slideOver,
                  pathParameters: {
                    'username': username,
                  },
                );
              },
              child: Text('Push Route', style: buttonTextStyle),
            ),
          ],
        ),
      ),
    );
  }
}

final textStyle = TextStyle(color: Colors.black, fontSize: 16);
final buttonTextStyle = textStyle.copyWith(color: Colors.white);

class SlideOverWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Slide In Widget'),
        backgroundColor: Colors.white,
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              'This is a screen with a back button, and I would like it to '
              'fill the whole viewport (expand outside the nested ',
              style: textStyle.copyWith(fontSize: textStyle.fontSize! + 2),
            ),
          ],
        ),
      ),
    );
  }
}
lulupointu commented 1 year ago

As I was saying you have to use VNester.stackedRoutes.

The only issue with this API is that is does not know how to redirect pop properly so you have to wire it manually using a VGuard for example.

Here is you example adapted to have the wanted behavior using `VGuard` ```dart import 'package:flutter/material.dart'; import 'package:vrouter/vrouter.dart'; void main() { runApp( VRouter( debugShowCheckedModeBanner: false, // VRouter acts as a MaterialApp mode: VRouterMode.history, // Remove the '#' from the url // logs: [VLogLevel.info], // Defines which logs to show, info is the default routes: [ VWidget( path: '/login', widget: LoginWidget(), stackedRoutes: [ ConnectedRoutes(), // Custom VRouteElement ], ), // This redirect every unknown routes to /login VRouteRedirector( redirectTo: '/login', path: r'*', ), ], ), ); } // Extend VRouteElementBuilder to create your own VRouteElement class ConnectedRoutes extends VRouteElementBuilder { static final String profile = 'profile'; static void toProfile(BuildContext context, String username) => context.vRouter.to('/$username/$profile'); static final String settings = 'settings'; static final String slideOver = '$settings/slideover'; static void toSettings(BuildContext context, String username) => context.vRouter.to('/$username/$settings'); @override List buildRoutes() { return [ VNester.builder( // .builder constructor gives you easy access to VRouter data path: '/:username', // :username is a path parameter and can be any value widgetBuilder: (_, state, child) => MyScaffold( child, currentIndex: state.names.contains(profile) ? 0 : 1, ), nestedRoutes: [ VWidget( path: profile, name: profile, widget: ProfileWidget(), ), VWidget( key: ValueKey(settings), path: settings, name: settings, aliases: [slideOver], widget: SettingsWidget(), // Custom transition buildTransition: (animation, ___, child) { return ScaleTransition( scale: animation, child: child, ); }, ), ], stackedRoutes: [ VGuard( beforeLeave: (vRedirector, _) async { final isGoingToSettings = vRedirector.newVRouterData?.names.contains(settings) ?? false; if (!isGoingToSettings) { vRedirector.toNamed( settings, pathParameters: vRedirector.previousVRouterData?.pathParameters ?? {}, ); } }, stackedRoutes: [ VWidget( name: slideOver, path: slideOver, widget: SlideOverWidget(), ), ], ) ], ), ]; } } class LoginWidget extends StatefulWidget { @override _LoginWidgetState createState() => _LoginWidgetState(); } class _LoginWidgetState extends State { String name = 'bob'; final _formKey = GlobalKey(); @override Widget build(BuildContext context) { return Material( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('Enter your name to connect: '), Expanded( child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.black), ), child: Form( key: _formKey, child: TextFormField( textAlign: TextAlign.center, onChanged: (value) => name = value, initialValue: 'bob', ), ), ), ), ], ), // This FAB is shared and shows hero animations working with no issues FloatingActionButton( heroTag: 'FAB', onPressed: () { setState(() => (_formKey.currentState!.validate()) ? ConnectedRoutes.toProfile(context, name) : null); }, child: Icon(Icons.login), ) ], ), ), ); } } class MyScaffold extends StatelessWidget { final Widget child; final int currentIndex; const MyScaffold(this.child, {required this.currentIndex}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('You are connected'), ), bottomNavigationBar: BottomNavigationBar( currentIndex: currentIndex, items: [ BottomNavigationBarItem( icon: Icon(Icons.person_outline), label: 'Profile'), BottomNavigationBarItem( icon: Icon(Icons.info_outline), label: 'Info'), ], onTap: (int index) { // We can access this username via the path parameters final username = VRouter.of(context).pathParameters['username']!; if (index == 0) { ConnectedRoutes.toProfile(context, username); } else { ConnectedRoutes.toSettings(context, username); } }, ), body: child, // This FAB is shared with login and shows hero animations working with no issues floatingActionButton: FloatingActionButton( heroTag: 'FAB', onPressed: () => VRouter.of(context).to('/login'), child: Icon(Icons.logout), ), ); } } class ProfileWidget extends StatefulWidget { @override _ProfileWidgetState createState() => _ProfileWidgetState(); } class _ProfileWidgetState extends State { int count = 0; @override Widget build(BuildContext context) { // VNavigationGuard allows you to react to navigation events locally return VWidgetGuard( // When entering or updating the route, we try to get the count from the local history state // This history state will be NOT null if the user presses the back button for example afterEnter: (context, __, ___) => getCountFromState(context), afterUpdate: (context, __, ___) => getCountFromState(context), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextButton( onPressed: () { VRouter.of(context).to( context.vRouter.url, isReplacement: true, historyState: {'count': '${count + 1}'}, ); setState(() => count++); }, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(50), color: Colors.blueAccent, ), padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0), child: Text( 'Your pressed this button $count times', style: buttonTextStyle, ), ), ), SizedBox(height: 20), Text( 'This number is saved in the history state so if you are on the web leave this page and hit the back button to see this number restored!', style: textStyle, textAlign: TextAlign.center, ), ], ), ), ), ); } void getCountFromState(BuildContext context) { setState(() { count = (VRouter.of(context).historyState['count'] == null) ? 0 : int.tryParse(VRouter.of(context).historyState['count'] ?? '') ?? 0; }); } } class SettingsWidget extends StatefulWidget { @override State createState() => _SettingsWidgetState(); } class _SettingsWidgetState extends State { var count = 0; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Press count: $count', style: textStyle.copyWith(fontSize: textStyle.fontSize! + 2), ), SizedBox(height: 20), ElevatedButton( onPressed: () { setState(() => count++); }, child: Text('Increment', style: buttonTextStyle), ), SizedBox(height: 20), ElevatedButton( onPressed: () { final username = VRouter.of(context).pathParameters['username']!; context.vRouter.toNamed( ConnectedRoutes.slideOver, pathParameters: { 'username': username, }, ); }, child: Text('Push Route', style: buttonTextStyle), ), ], ), ), ); } } final textStyle = TextStyle(color: Colors.black, fontSize: 16); final buttonTextStyle = textStyle.copyWith(color: Colors.white); class SlideOverWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Slide In Widget'), backgroundColor: Colors.white, ), body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'This is a screen with a back button, and I would like it to ' 'fill the whole viewport (expand outside the nested ', style: textStyle.copyWith(fontSize: textStyle.fontSize! + 2), ), ], ), ), ); } } ```
oravecz commented 1 year ago

Thanks for the working example. This wasn't intuitive for me, so please confirm these generalizations I am drawing from your code.

  1. There is no way for stacked (or nested) routes to display outside the bounds of a nested route they are contained within.
  2. Therefore, if you have a VWidget that you would like to display outside of VNested's widget (let's call it a pop over), you cannot declare it inside any part of the nestedRoutes property hierarchy.
  3. The nested route from where you will invoke the popover has to declare a Key and declare an alias for the pop over.
  4. The pop over has to be prefixed with the path to the VWidget from where it will be navigated, because
  5. A guard for the popover will be controlling the pop()behavior by manually inspecting the url path to determine the destination

1 and #2 are intuitive to me, but I have some follow-ups.

  1. Why did you add a Key to the Settings Route? (I really don't understand when to add a key or not in the routes, but it doesn't seem like it is needed here)
  2. Why the alias on the Settings route?
  3. The guard is basically hard-coding a return to the settings page if any navigation takes place in the pop over. a. How would I reuse the pop over from multiple nested routes? b. How would I navigate to a page other than settings from the pop over (I added a nav to profile button to the pop over to demonstrate that it always navigates to settings.
  4. In the guard, why check if settings is in the url paths? It seems to prevent me from navigating anywhere else than to a url under the settings pages.
Updated example ```dart import 'package:flutter/material.dart'; import 'package:vrouter/vrouter.dart'; void main() { runApp( VRouter( debugShowCheckedModeBanner: false, // VRouter acts as a MaterialApp mode: VRouterMode.history, // Remove the '#' from the url // logs: [VLogLevel.info], // Defines which logs to show, info is the default routes: [ VWidget( path: '/login', widget: LoginWidget(), stackedRoutes: [ ConnectedRoutes(), // Custom VRouteElement ], ), // This redirect every unknown routes to /login VRouteRedirector( redirectTo: '/login', path: r'*', ), ], ), ); } // Extend VRouteElementBuilder to create your own VRouteElement class ConnectedRoutes extends VRouteElementBuilder { static final String profile = 'profile'; static void toProfile(BuildContext context, String username) => context.vRouter.to('/$username/$profile'); static final String settings = 'settings'; static final String slideOver = '$settings/slideover'; static void toSettings(BuildContext context, String username) => context.vRouter.to('/$username/$settings'); @override List buildRoutes() { return [ VNester.builder( // .builder constructor gives you easy access to VRouter data path: '/:username', // :username is a path parameter and can be any value widgetBuilder: (_, state, child) => MyScaffold( child, currentIndex: state.names.contains(profile) ? 0 : 1, ), nestedRoutes: [ VWidget( path: profile, name: profile, widget: ProfileWidget(), ), VWidget( key: ValueKey(settings), path: settings, name: settings, aliases: [slideOver], widget: SettingsWidget(), // Custom transition buildTransition: (animation, ___, child) { return ScaleTransition( scale: animation, child: child, ); }, ), ], stackedRoutes: [ VGuard( beforeLeave: (vRedirector, _) async { final isGoingToSettings = vRedirector.newVRouterData?.names.contains(settings) ?? false; if (!isGoingToSettings) { vRedirector.toNamed( settings, pathParameters: vRedirector.previousVRouterData?.pathParameters ?? {}, ); } }, stackedRoutes: [ VWidget( name: slideOver, path: slideOver, widget: SlideOverWidget(), ), ], ) ], ), ]; } } class LoginWidget extends StatefulWidget { @override _LoginWidgetState createState() => _LoginWidgetState(); } class _LoginWidgetState extends State { String name = 'bob'; final _formKey = GlobalKey(); @override Widget build(BuildContext context) { return Material( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('Enter your name to connect: '), Expanded( child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.black), ), child: Form( key: _formKey, child: TextFormField( textAlign: TextAlign.center, onChanged: (value) => name = value, initialValue: 'bob', ), ), ), ), ], ), // This FAB is shared and shows hero animations working with no issues FloatingActionButton( heroTag: 'FAB', onPressed: () { setState(() => (_formKey.currentState!.validate()) ? ConnectedRoutes.toProfile(context, name) : null); }, child: Icon(Icons.login), ) ], ), ), ); } } class MyScaffold extends StatelessWidget { final Widget child; final int currentIndex; const MyScaffold(this.child, {required this.currentIndex}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('You are connected'), ), bottomNavigationBar: BottomNavigationBar( currentIndex: currentIndex, items: [ BottomNavigationBarItem( icon: Icon(Icons.person_outline), label: 'Profile'), BottomNavigationBarItem( icon: Icon(Icons.info_outline), label: 'Info'), ], onTap: (int index) { // We can access this username via the path parameters final username = VRouter.of(context).pathParameters['username']!; if (index == 0) { ConnectedRoutes.toProfile(context, username); } else { ConnectedRoutes.toSettings(context, username); } }, ), body: child, // This FAB is shared with login and shows hero animations working with no issues floatingActionButton: FloatingActionButton( heroTag: 'FAB', onPressed: () => VRouter.of(context).to('/login'), child: Icon(Icons.logout), ), ); } } class ProfileWidget extends StatefulWidget { @override _ProfileWidgetState createState() => _ProfileWidgetState(); } class _ProfileWidgetState extends State { int count = 0; @override Widget build(BuildContext context) { // VNavigationGuard allows you to react to navigation events locally return VWidgetGuard( // When entering or updating the route, we try to get the count from the local history state // This history state will be NOT null if the user presses the back button for example afterEnter: (context, __, ___) => getCountFromState(context), afterUpdate: (context, __, ___) => getCountFromState(context), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextButton( onPressed: () { VRouter.of(context).to( context.vRouter.url, isReplacement: true, historyState: {'count': '${count + 1}'}, ); setState(() => count++); }, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(50), color: Colors.blueAccent, ), padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0), child: Text( 'Your pressed this button $count times', style: buttonTextStyle, ), ), ), SizedBox(height: 20), Text( 'This number is saved in the history state so if you are on the web leave this page and hit the back button to see this number restored!', style: textStyle, textAlign: TextAlign.center, ), ], ), ), ), ); } void getCountFromState(BuildContext context) { setState(() { count = (VRouter.of(context).historyState['count'] == null) ? 0 : int.tryParse(VRouter.of(context).historyState['count'] ?? '') ?? 0; }); } } class SettingsWidget extends StatefulWidget { @override State createState() => _SettingsWidgetState(); } class _SettingsWidgetState extends State { var count = 0; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Press count: $count', style: textStyle.copyWith(fontSize: textStyle.fontSize! + 2), ), SizedBox(height: 20), ElevatedButton( onPressed: () { setState(() => count++); }, child: Text('Increment', style: buttonTextStyle), ), SizedBox(height: 20), ElevatedButton( onPressed: () { final username = VRouter.of(context).pathParameters['username']!; context.vRouter.toNamed( ConnectedRoutes.slideOver, pathParameters: { 'username': username, }, ); }, child: Text('Push Route', style: buttonTextStyle), ), ], ), ), ); } } final textStyle = TextStyle(color: Colors.black, fontSize: 16); final buttonTextStyle = textStyle.copyWith(color: Colors.white); class SlideOverWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( title: Text('Slide In Widget'), ), body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'This is a screen with a back button, and I would like it to ' 'fill the whole viewport (expand outside the nested ', style: textStyle.copyWith(fontSize: textStyle.fontSize! + 2), ), SizedBox(height: 20), ElevatedButton( onPressed: () { final username = VRouter.of(context).pathParameters['username']!; context.vRouter.toNamed( ConnectedRoutes.profile, pathParameters: { 'username': username, }, ); }, child: Text('Profile', style: buttonTextStyle), ), ], ), ), ); } } ```
lulupointu commented 1 year ago

Concerning your assertions:

  1. True
  2. True
  3. True
  4. False, it's just that most of the time it make sense
  5. Actually using a guard is a bad idea, see my answer to you other questions bellow

Answer to your follow-ups:

  1. If a route represents the same place but with different urls, then you add a key. For example:
    • /bob/profile and /alice/profile are 2 possible path of VNester (because VNester path is :username) which represent 2 differents places. Therefore you do not add a key to VNester. The effect is that the state of VNester and everything bellow would be reset if your switched from /bob/profile to /alice/profile (which is what you want)
    • /bob/settings and /bob/settings/slideover represent the same settings, therefore you use a key. The concrete effect is that you won't loose your Settings state when you go to slideover
  2. The alias is used to indicate that when the current url is /bob/settings/slideover, then the settings should be shown inside VNester. Basically, every time you use nestedRoutes and stackedRoute you have to make sure that there is always a matching route in nestedRoutes so that VNester knows what to display there
  3. You can check my example at the end for a concrete example but basically a. You can use a logic where you always have the popover path being pathOfNested + '/popover' and where you hard code the pop to nativate to the currentPath - '/popover' b. Using VGuard was a bad idea because it impacted every natigation. The good think to do is to use WillPopScope to only influence the pop. Warning: I think you would want to use BackButtonListener as well to override android back button behavior to do the same as pop.
  4. Same as 3.b
Updated example ```dart import 'package:flutter/material.dart'; import 'package:vrouter/vrouter.dart'; void main() { runApp( VRouter( debugShowCheckedModeBanner: false, // VRouter acts as a MaterialApp mode: VRouterMode.history, // Remove the '#' from the url // logs: [VLogLevel.info], // Defines which logs to show, info is the default routes: [ VWidget( path: '/login', widget: LoginWidget(), stackedRoutes: [ ConnectedRoutes(), // Custom VRouteElement ], ), // This redirect every unknown routes to /login VRouteRedirector( redirectTo: '/login', path: r'*', ), ], ), ); } // Extend VRouteElementBuilder to create your own VRouteElement class ConnectedRoutes extends VRouteElementBuilder { static final String profile = 'profile'; static void toProfile(BuildContext context, String username) => context.vRouter.to('/$username/$profile'); static final String settings = 'settings'; static final String slideOver = 'slideover'; static void toSettings(BuildContext context, String username) => context.vRouter.to('/$username/$settings'); @override List buildRoutes() { return [ VNester.builder( // .builder constructor gives you easy access to VRouter data path: '/:username', // :username is a path parameter and can be any value widgetBuilder: (_, state, child) => MyScaffold( child, currentIndex: state.names.contains(profile) ? 0 : 1, ), nestedRoutes: [ VWidget( key: ValueKey(profile), path: profile, name: profile, aliases: ['$profile/$slideOver'], widget: ProfileWidget(), ), VWidget( key: ValueKey(settings), path: settings, name: settings, aliases: ['$settings/$slideOver'], widget: SettingsWidget(), // Custom transition buildTransition: (animation, ___, child) { return ScaleTransition( scale: animation, child: child, ); }, ), ], stackedRoutes: [ VWidget( path: '(.*?)/$slideOver', widget: Builder( builder: (context) { return WillPopScope( onWillPop: () async { final vRouter = VRouter.of(context); vRouter.to(vRouter.path.replaceFirst('/$slideOver', '')); return false; }, child: BackButtonListener( onBackButtonPressed: () async { final vRouter = VRouter.of(context); vRouter.to(vRouter.path.replaceFirst('/$slideOver', '')); return true; }, child: SlideOverWidget(), ), ); }, ), ), ], ), ]; } } class LoginWidget extends StatefulWidget { @override _LoginWidgetState createState() => _LoginWidgetState(); } class _LoginWidgetState extends State { String name = 'bob'; final _formKey = GlobalKey(); @override Widget build(BuildContext context) { return Material( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('Enter your name to connect: '), Expanded( child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.black), ), child: Form( key: _formKey, child: TextFormField( textAlign: TextAlign.center, onChanged: (value) => name = value, initialValue: 'bob', ), ), ), ), ], ), // This FAB is shared and shows hero animations working with no issues FloatingActionButton( heroTag: 'FAB', onPressed: () { setState(() => (_formKey.currentState!.validate()) ? ConnectedRoutes.toProfile(context, name) : null); }, child: Icon(Icons.login), ) ], ), ), ); } } class MyScaffold extends StatelessWidget { final Widget child; final int currentIndex; const MyScaffold(this.child, {required this.currentIndex}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('You are connected'), ), bottomNavigationBar: BottomNavigationBar( currentIndex: currentIndex, items: [ BottomNavigationBarItem( icon: Icon(Icons.person_outline), label: 'Profile'), BottomNavigationBarItem( icon: Icon(Icons.info_outline), label: 'Info'), ], onTap: (int index) { // We can access this username via the path parameters final username = VRouter.of(context).pathParameters['username']!; if (index == 0) { ConnectedRoutes.toProfile(context, username); } else { ConnectedRoutes.toSettings(context, username); } }, ), body: child, // This FAB is shared with login and shows hero animations working with no issues floatingActionButton: FloatingActionButton( heroTag: 'FAB', onPressed: () => VRouter.of(context).to('/login'), child: Icon(Icons.logout), ), ); } } class ProfileWidget extends StatefulWidget { @override _ProfileWidgetState createState() => _ProfileWidgetState(); } class _ProfileWidgetState extends State { int count = 0; @override Widget build(BuildContext context) { // VNavigationGuard allows you to react to navigation events locally return VWidgetGuard( // When entering or updating the route, we try to get the count from the local history state // This history state will be NOT null if the user presses the back button for example afterEnter: (context, __, ___) => getCountFromState(context), afterUpdate: (context, __, ___) => getCountFromState(context), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextButton( onPressed: () { VRouter.of(context).to( context.vRouter.url, isReplacement: true, historyState: {'count': '${count + 1}'}, ); setState(() => count++); }, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(50), color: Colors.blueAccent, ), padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0), child: Text( 'Your pressed this button $count times', style: buttonTextStyle, ), ), ), SizedBox(height: 20), Text( 'This number is saved in the history state so if you are on the web leave this page and hit the back button to see this number restored!', style: textStyle, textAlign: TextAlign.center, ), SizedBox(height: 20), ElevatedButton( onPressed: () { context.vRouter.to( '${context.vRouter.path}/${ConnectedRoutes.slideOver}'); }, child: Text('Push Route', style: buttonTextStyle), ), ], ), ), ), ); } void getCountFromState(BuildContext context) { setState(() { count = (VRouter.of(context).historyState['count'] == null) ? 0 : int.tryParse(VRouter.of(context).historyState['count'] ?? '') ?? 0; }); } } class SettingsWidget extends StatefulWidget { @override State createState() => _SettingsWidgetState(); } class _SettingsWidgetState extends State { var count = 0; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Press count: $count', style: textStyle.copyWith(fontSize: textStyle.fontSize! + 2), ), SizedBox(height: 20), ElevatedButton( onPressed: () { setState(() => count++); }, child: Text('Increment', style: buttonTextStyle), ), SizedBox(height: 20), ElevatedButton( onPressed: () { context.vRouter .to('${context.vRouter.path}/${ConnectedRoutes.slideOver}'); }, child: Text('Push Route', style: buttonTextStyle), ), ], ), ), ); } } final textStyle = TextStyle(color: Colors.black, fontSize: 16); final buttonTextStyle = textStyle.copyWith(color: Colors.white); class SlideOverWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( title: Text('Slide In Widget'), ), body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'This is a screen with a back button, and I would like it to ' 'fill the whole viewport (expand outside the nested ', style: textStyle.copyWith(fontSize: textStyle.fontSize! + 2), ), SizedBox(height: 20), ElevatedButton( onPressed: () { final username = VRouter.of(context).pathParameters['username']!; context.vRouter.toNamed( ConnectedRoutes.profile, pathParameters: { 'username': username, }, ); }, child: Text('Profile', style: buttonTextStyle), ), ], ), ), ); } } ```
lulupointu commented 1 year ago

Closing since I answered every questions. Feel free to reopen if some doubts persist.