lulupointu / vrouter

MIT License
202 stars 38 forks source link

Can a top-level route also be a stacked route? #214

Closed oravecz closed 1 year ago

oravecz commented 1 year ago

In the example, I have an /about route which is triggered by clicking on the question icon in the AppBar. I want the AboutScreen to be pushed over the nested routes, so I am declaring it as a peer route alongside the nested routes.

Visually, this is working for me as regardless of where you are in the routing, clicking on the AppBar about icon navigates to the AboutScreen, however the pop() behavior is broken which I believe is a result of how I made the AboutRoute function like a stacked route, even though it is a top-level route.

      VWidget(
        path: about,
        widget: Container(),
        stackedRoutes: [
          VPopHandler(
            onPop: (vRedirector) async {
              // This approach works, but will it return to the previous url
              // consistently?
              final url = vRedirector.previousVRouterData?.previousUrl;

              if (url != null) {
                final queryParams =
                    vRedirector.previousVRouterData!.queryParameters;
                vRedirector.to(url, queryParameters: queryParams);
              }
            },
            stackedRoutes: [
              VWidget(path: null, widget: AboutScreen()),
            ],
          )
        ],
      )
Full example ```dart import 'package:flutter/material.dart'; import 'package:vrouter/vrouter.dart'; main() { runApp( VRouter( initialUrl: ProfileRoute.profile, routes: [ ShopRoute(), ProfileRoute(), AboutRoute(), ], debugShowCheckedModeBanner: false, ), ); } /// This is a top-level route defined outside of any nested route because I want /// it to display on top of all UX (fill the viewport). In order for it to /// properly set the `canPop` check, I have to place the VRoute into a /// `stackedRoutes` property. Note: I cannot simply place it into /// `VPopHandler.stackedRoutes`, but instead I have to nest it into a dummy /// `VWidget` with a `Container` as its widget that is never seen. class AboutRoute extends VRouteElementBuilder { static final String about = '/about'; @override List buildRoutes() { return [ VWidget(path: about, widget: Container(), stackedRoutes: [ VPopHandler( onPop: (vRedirector) async { // TODO: Why doesn't the standard onPop handler deal with this // case? Performing a `context.vRouter.pop()` navigates to // '/about' (same as current location). I feel this problem is // related to the nesting of the AboutScreen inside a dummy VWidget // // This approach works, but will it return to the previous url // consistently? final url = vRedirector.previousVRouterData?.previousUrl; if (url != null) { final queryParams = vRedirector.previousVRouterData!.queryParameters; vRedirector.to( url, queryParameters: queryParams ); } }, stackedRoutes: [ VWidget(path: null, widget: AboutScreen()), ], ) ]), ]; } } class ShopRoute extends VRouteElementBuilder { static final int index = 0; static final String shop = '/shop'; static final String order = shop + '/order'; @override List buildRoutes() { return [ ScaffoldRouteElement( path: shop, index: index, nestedRoutes: [ VWidget( path: null, // TODO: I assume this alias has to exist because the VNester has // to be in the path even for it's stackedRoutes to be discovered? // Would it be better practice to use a wildcard to cover all paths // which are subroutes of this VNester? aliases: [shop + '/*'], widget: ShopScreen(), key: ValueKey('Shop'), ), ], stackedRoutes: [VWidget(path: order, widget: OrderScreen())], ) ]; } } class ProfileRoute extends VRouteElementBuilder { static final int index = 1; static final String profile = '/profile'; static final String settings = profile + '/settings'; @override List buildRoutes() { return [ ScaffoldRouteElement( path: profile, index: index, nestedRoutes: [ VWidget( path: null, aliases: [settings], widget: ProfileScreen(), // TODO: Source comments indicate assigning this key will prevent // animation of the ProfileScreen when navigating from // ProfileScreen to SettingsScreen. Is that the purpose in this // case? (I didn't notice a difference in behavior whether the key // is specified or not. I assume the default value is achieving // the same behavior?) key: ValueKey('Profile'), stackedRoutes: [ VWidget(path: settings, widget: SettingsScreen()), ], ), ], ) ]; } } class ScaffoldRouteElement extends VRouteElementBuilder { static final navigatorKey = GlobalKey(); final String path; final int index; final List nestedRoutes; final List? stackedRoutes; ScaffoldRouteElement({ required this.path, required this.index, required this.nestedRoutes, this.stackedRoutes, }); @override List buildRoutes() { return [ VNester( // According to docs, the same key and navigatorKey ensure that the two // instances of this VNester will retain the same RenderObject; I am not // noticing any difference with the next two lines commented out or not, // but I am not doing anything stateful in my example key: ValueKey('MyScaffold'), navigatorKey: navigatorKey, path: path, widgetBuilder: (child) => MyScaffold(child: child, index: index), nestedRoutes: nestedRoutes, stackedRoutes: stackedRoutes ?? [], ) ]; } } class MyScaffold extends StatelessWidget { final Widget child; final int index; const MyScaffold({required this.child, required this.index}); @override Widget build(BuildContext context) { final title = index == ProfileRoute.index ? "Profile Scaffold" : index == ShopRoute.index ? 'Shop Scaffold' : 'Unknown Scaffold'; return Scaffold( backgroundColor: Colors.white, // TODO: Why is the AppBar's back button not visible when canPop is true? appBar: AppBar( title: Text(title), actions: [ Padding( padding: const EdgeInsets.only(right: 24), child: IconButton( // TODO: how do I make canPop return true when this route loads? onPressed: () => context.vRouter.to(AboutRoute.about), icon: Icon(Icons.question_mark), ), ), ], ), bottomNavigationBar: BottomNavigationBar( currentIndex: index, items: [ BottomNavigationBarItem(icon: Icon(Icons.shop), label: 'Shop'), BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), ], onTap: (int index) { if (index == ProfileRoute.index) { context.vRouter.to(ProfileRoute.profile); } else if (index == ShopRoute.index) { context.vRouter.to(ShopRoute.shop); } else { throw Exception('Unknown navigation target'); } }, ), body: child, ); } } abstract class BaseWidget extends StatelessWidget { String get title; String get buttonText; String get to; @override Widget build(BuildContext context) { final canPop = ModalRoute.of(context)?.canPop; return Material( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('$title Screen'), SizedBox(height: 50), ElevatedButton( onPressed: () => context.vRouter.to(to), child: Text(buttonText), ), SizedBox(height: 50), Text('canPop? $canPop'), if (canPop == true) ElevatedButton( onPressed: () { return context.vRouter.pop(); }, child: Text('Pop'), ), ], ), ), ); } } class ShopScreen extends BaseWidget { @override String get title => 'Shop'; @override String get buttonText => 'Go to order (stacked outside nested path)'; @override String get to => ShopRoute.order; } class OrderScreen extends BaseWidget { @override String get title => 'Order'; @override String get buttonText => 'Go to Settings (stacked in a different path)'; @override String get to => ProfileRoute.settings; } class SettingsScreen extends BaseWidget { @override String get title => 'Settings'; @override String get buttonText => 'Go to Order'; @override String get to => ShopRoute.order; } class ProfileScreen extends BaseWidget { @override String get title => 'Profile'; @override String get buttonText => 'Go to Settings (stacked in nested path)'; @override String get to => ProfileRoute.settings; } class AboutScreen extends BaseWidget { @override String get title => 'About'; @override String get buttonText => 'Go to Profile'; @override String get to => ProfileRoute.profile; } ```
lulupointu commented 1 year ago

I would advise you to handle the pop the way I showed you in this reply

lulupointu commented 1 year ago

Closing this since my last comment specifies my recommendation. Feel free to reopen if needed.