Stacked-Org / stacked

A Flutter application architecture created from real world scenarios
MIT License
978 stars 255 forks source link

[bug]: Nested router navigation using navigator v1 #1036

Closed scognito closed 1 month ago

scognito commented 1 year ago

Describe the bug

In the compay I'm working on, I'm trying to upgrade from old versions of stacked (2.2.8) / stacked_generator (0.5.7). I don't have problem navigating to parent routes, but once I try to navigate to a nested one I get an exception: Could not find a generator for route RouteSettings("giftguide", {"groupId": "myGroup", "key": "null"}) in the _WidgetsAppState. I made a quick sample to reproduce the problem, the navigation stack is Home -> ShopScreen -> GiftGuide

The error I get is

[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: Could not find a generator for route RouteSettings("giftguide", {"groupId": "myGroup", "key": "null"}) in the _WidgetsAppState.
Make sure your root app widget has provided a way to generate
this route.
Generators for routes are searched for in the following order:
 1. For the "/" route, the "home" property, if non-null, is used.
 2. Otherwise, the "routes" table is used, if it has an entry for the route.
 3. Otherwise, onGenerateRoute is called. It should return a non-null value for any valid route not handled by "home" and "routes".
 4. Finally if all else fails onUnknownRoute is called.
Unfortunately, onUnknownRoute was not set.
#0      _WidgetsAppState._onUnknownRoute.<anonymous closure> (package:flutter/src/widgets/app.dart:1438:9)
#1      _WidgetsAppState._onUnknownRoute (package:flutter/src/widgets/app.dart:1453:6)
#2      NavigatorState._routeNamed (package:flutter/src/widgets/navigator.dart:4231:37)
#3      NavigatorState.pushNamed (<…>

Is there is something I'm missing? Thank you

To reproduce

build.yaml

targets:
  $default:
    builders:
      stacked_generator|stackedRouterGenerator:
        options:
          navigator2: false

main.dart

Future main() async {
  WidgetsFlutterBinding.ensureInitialized();

  setupLocator();

  const initialRoute = HomeScreen();

  runApp(
    App(
      initialRoute: initialRoute,
    ),
  );
}

class App extends StatelessWidget {
  final Widget initialRoute;

  App({
    Key? key,
    required this.initialRoute,
  }) : super(key: key ?? UniqueKey());

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: initialRoute,
      onGenerateRoute: StackedRouter().onGenerateRoute,
      navigatorKey: StackedService.navigatorKey,
    );
  }
}

shop_screen.dart:

class ShopScreen extends StatelessWidget {
  const ShopScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('SHOP'),
      ),
      body: Center(
        child: Column(
          children: [
            IndexButton(
              text: 'GO TO GIFTGUIDE',
              onTap: () {
                locator<NavigationService>().navigateToNestedGiftGuideScreenInShopScreenRouter(groupId: 'myGroup');
              },
            ),
          ],
        ),
      ),
    );
  }
}

router.dart:

@StackedApp(
  routes: [
    MaterialRoute(
      //path: '/home',
      page: HomeScreen,
      // initial: true,
    ),
    MaterialRoute(
      path: '/shop',
      page: ShopScreen,
      children: [
        MaterialRoute(
          path: 'giftguide',
          page: GiftGuideScreen,
        ),
      ],
    ),
    MaterialRoute(
      path: '/explore',
      page: ExploreNavigationStackScreen,
    )
  ],
)
class App {
  // This class has no purpose besides housing the annotation that generates the required functionality
}

router.router.dart:

class Routes {
  static const homeScreen = '/home-screen';

  static const shopScreen = '/shop';

  static const exploreNavigationStackScreen = '/explore';

  static const all = <String>{
    homeScreen,
    shopScreen,
    exploreNavigationStackScreen,
  };
}

class StackedRouter extends _i1.RouterBase {
  final _routes = <_i1.RouteDef>[
    _i1.RouteDef(
      Routes.homeScreen,
      page: _i2.HomeScreen,
    ),
    _i1.RouteDef(
      Routes.shopScreen,
      page: _i3.ShopScreen,
    ),
    _i1.RouteDef(
      Routes.exploreNavigationStackScreen,
      page: _i4.ExploreNavigationStackScreen,
    ),
  ];

  final _pagesMap = <Type, _i1.StackedRouteFactory>{
    _i2.HomeScreen: (data) {
      return _i5.MaterialPageRoute<dynamic>(
        builder: (context) => const _i2.HomeScreen(),
        settings: data,
      );
    },
    _i3.ShopScreen: (data) {
      return _i5.MaterialPageRoute<dynamic>(
        builder: (context) => const _i3.ShopScreen(),
        settings: data,
      );
    },
    _i4.ExploreNavigationStackScreen: (data) {
      return _i5.MaterialPageRoute<dynamic>(
        builder: (context) => const _i4.ExploreNavigationStackScreen(),
        settings: data,
      );
    },
  };

  @override
  List<_i1.RouteDef> get routes => _routes;

  @override
  Map<Type, _i1.StackedRouteFactory> get pagesMap => _pagesMap;
}

class ShopScreenRoutes {
  static const giftGuideScreen = 'giftguide';

  static const all = <String>{giftGuideScreen};
}

class ShopScreenRouter extends _i1.RouterBase {
  final _routes = <_i1.RouteDef>[
    _i1.RouteDef(
      ShopScreenRoutes.giftGuideScreen,
      page: _i6.GiftGuideScreen,
    )
  ];

  final _pagesMap = <Type, _i1.StackedRouteFactory>{
    _i6.GiftGuideScreen: (data) {
      final args = data.getArgs<NestedGiftGuideScreenArguments>(nullOk: false);
      return _i5.MaterialPageRoute<dynamic>(
        builder: (context) =>
            _i6.GiftGuideScreen(groupId: args.groupId, key: args.key),
        settings: data,
      );
    }
  };

  @override
  List<_i1.RouteDef> get routes => _routes;

  @override
  Map<Type, _i1.StackedRouteFactory> get pagesMap => _pagesMap;
}

class NestedGiftGuideScreenArguments {
  const NestedGiftGuideScreenArguments({
    required this.groupId,
    this.key,
  });

  final String groupId;

  final _i5.Key? key;

  @override
  String toString() {
    return '{"groupId": "$groupId", "key": "$key"}';
  }

  @override
  bool operator ==(covariant NestedGiftGuideScreenArguments other) {
    if (identical(this, other)) return true;
    return other.groupId == groupId && other.key == key;
  }

  @override
  int get hashCode {
    return groupId.hashCode ^ key.hashCode;
  }
}

extension NavigatorStateExtension on _i7.NavigationService {
  Future<dynamic> navigateToHomeScreen([
    int? routerId,
    bool preventDuplicates = true,
    Map<String, String>? parameters,
    Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
        transition,
  ]) async {
    return navigateTo<dynamic>(Routes.homeScreen,
        id: routerId,
        preventDuplicates: preventDuplicates,
        parameters: parameters,
        transition: transition);
  }

  Future<dynamic> navigateToShopScreen([
    int? routerId,
    bool preventDuplicates = true,
    Map<String, String>? parameters,
    Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
        transition,
  ]) async {
    return navigateTo<dynamic>(Routes.shopScreen,
        id: routerId,
        preventDuplicates: preventDuplicates,
        parameters: parameters,
        transition: transition);
  }

  Future<dynamic> navigateToExploreNavigationStackScreen([
    int? routerId,
    bool preventDuplicates = true,
    Map<String, String>? parameters,
    Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
        transition,
  ]) async {
    return navigateTo<dynamic>(Routes.exploreNavigationStackScreen,
        id: routerId,
        preventDuplicates: preventDuplicates,
        parameters: parameters,
        transition: transition);
  }

  Future<dynamic> navigateToNestedGiftGuideScreenInShopScreenRouter({
    required String groupId,
    _i5.Key? key,
    int? routerId,
    bool preventDuplicates = true,
    Map<String, String>? parameters,
    Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
        transition,
  }) async {
    return navigateTo<dynamic>(ShopScreenRoutes.giftGuideScreen,
        arguments: NestedGiftGuideScreenArguments(groupId: groupId, key: key),
        id: routerId,
        preventDuplicates: preventDuplicates,
        parameters: parameters,
        transition: transition);
  }

  Future<dynamic> replaceWithHomeScreen([
    int? routerId,
    bool preventDuplicates = true,
    Map<String, String>? parameters,
    Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
        transition,
  ]) async {
    return replaceWith<dynamic>(Routes.homeScreen,
        id: routerId,
        preventDuplicates: preventDuplicates,
        parameters: parameters,
        transition: transition);
  }

  Future<dynamic> replaceWithShopScreen([
    int? routerId,
    bool preventDuplicates = true,
    Map<String, String>? parameters,
    Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
        transition,
  ]) async {
    return replaceWith<dynamic>(Routes.shopScreen,
        id: routerId,
        preventDuplicates: preventDuplicates,
        parameters: parameters,
        transition: transition);
  }

  Future<dynamic> replaceWithExploreNavigationStackScreen([
    int? routerId,
    bool preventDuplicates = true,
    Map<String, String>? parameters,
    Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
        transition,
  ]) async {
    return replaceWith<dynamic>(Routes.exploreNavigationStackScreen,
        id: routerId,
        preventDuplicates: preventDuplicates,
        parameters: parameters,
        transition: transition);
  }

  Future<dynamic> replaceWithNestedGiftGuideScreenInShopScreenRouter({
    required String groupId,
    _i5.Key? key,
    int? routerId,
    bool preventDuplicates = true,
    Map<String, String>? parameters,
    Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
        transition,
  }) async {
    return replaceWith<dynamic>(ShopScreenRoutes.giftGuideScreen,
        arguments: NestedGiftGuideScreenArguments(groupId: groupId, key: key),
        id: routerId,
        preventDuplicates: preventDuplicates,
        parameters: parameters,
        transition: transition);
  }
}

Expected behavior

I expect to navigate to the GiftGuide screen

Screenshots

No response

Additional Context

No response

FilledStacks commented 1 year ago

There's an additional method to navigateToNestedRoute you should use that instead of the navigateToRoute function as that expects a top level route, or a route available in the current navigator.