flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
162.97k stars 26.8k forks source link

[go_router] StatefulShellBranch glitch when I try to push an inner route from the other route #141267

Open subzero911 opened 5 months ago

subzero911 commented 5 months ago

go_router: ^13.0.1

Steps to reproduce

I went to /dev screen and tried to push /wallet/card-ready-banner-child from it

Expected results

This screen is just pushed on the top of the root navigator stack.

Actual results

Some assertion fails

image

When I'm repeating it the 2nd time, there's an another error:

image

Code sample

Code sample I've created the following setup: image

Router setup

image

Logs

No response

Flutter Doctor output

Doctor output ```console [✓] Flutter (Channel stable, 3.16.2, on macOS 13.0.1 22A400 darwin-arm64, locale ru-RU) • Flutter version 3.16.2 on channel stable at /Users/svmolchan1/fvm/versions/stable • Upstream repository https://github.com/flutter/flutter.git • Framework revision 9e1c857886 (6 weeks ago), 2023-11-30 11:51:18 -0600 • Engine revision cf7a9d0800 • Dart version 3.2.2 • DevTools version 2.28.3 [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.1) • Android SDK at /Users/svmolchan1/Library/Android/sdk • Platform android-33, build-tools 33.0.1 • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 14.3.1) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 14E300c • CocoaPods version 1.14.3 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2021.3) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866) [✓] VS Code (version 1.85.1) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.80.0 [✓] Connected device (3 available) • M2101K9AG (mobile) • cc706425 • android-arm64 • Android 13 (API 33) • macOS (desktop) • macos • darwin-arm64 • macOS 13.0.1 22A400 darwin-arm64 • Chrome (web) • chrome • web-javascript • Google Chrome 116.0.5845.96 [✓] Network resources • All expected network resources are available. ```
sc-ot commented 5 months ago

Did you pass a navigatorKey to your StatefulShellBranch, so it can navigate independently from the root Navigator

final _walletNavigatorKey = GlobalKey<NavigatorState>();

 StatefulShellBranch(
            navigatorKey: _walletNavigatorKey,
            ...

As mentioned in route.dart:

/// The [GlobalKey] to be used by the [Navigator] built for this branch. /// /// A separate Navigator will be built for each StatefulShellBranch in a /// [StatefulShellRoute] and this key will be used to identify the Navigator. /// The routes associated with this branch will be placed o onto that /// Navigator instead of the root Navigator.

subzero911 commented 5 months ago

No I didn't. Do you mean that it's necessary to mark every branch with its own GlobalKey?

subzero911 commented 5 months ago

No, I added a GlobalKey and it didn't help

image image image image
sc-ot commented 5 months ago

Thats what i do atleast with my StatefulShellBranches and it works. But somehow it also works without the NavigatorKey in my project ... can you add your router config as code?

subzero911 commented 5 months ago

Here you are. There's a fragment of my GoRouter setup:

```dart final rootNavigatorKey = GlobalKey(); // GoRouter configuration final router = GoRouter( navigatorKey: rootNavigatorKey, debugLogDiagnostics: true, initialLocation: getInitialRoute(), routes: [ GoRoute( path: '/dev', // экран разработчика builder: (context, state) { return DevScreen(); }, ), StatefulShellRoute.indexedStack( branches: [ StatefulShellBranch( routes: [ GoRoute( name: RouteNames.taskTracker, path: '/tasks', redirect: loginRedirect, builder: (context, state) => TasksScreen(), routes: [ GoRoute( name: RouteNames.newTask, path: 'new-task', parentNavigatorKey: rootNavigatorKey, pageBuilder: (context, state) { return CupertinoPage( fullscreenDialog: true, child: GoRouteWrapper( onInit: () => GetIt.I.registerSingleton( TaskEditingController( state.extra as Task, GetIt.I(), ), ), onDispose: (context) => GetIt.I.unregister(), child: EditTaskScreen(), ), ); }, ), GoRoute( name: RouteNames.editTask, path: 'edit-task', parentNavigatorKey: rootNavigatorKey, pageBuilder: (context, state) { return CupertinoPage( fullscreenDialog: true, child: GoRouteWrapper( onInit: () => GetIt.I.registerSingleton( TaskEditingController( state.extra as Task, GetIt.I(), )..mode = TaskScreenMode.edit, ), onDispose: (context) => GetIt.I.unregister(), child: EditTaskScreen(), ), ); }, ), GoRoute( name: RouteNames.filteredTasks, path: 'filtered-tasks', pageBuilder: (context, state) { return CupertinoPage( child: GoRouteWrapper( onInit: () => GetIt.I.registerSingleton( FilteredTasksController( taskRepository: GetIt.I(), taskController: GetIt.I(), userController: GetIt.I(), currentStatus: state.extra as TaskStatus?, ), ), onDispose: (context) => GetIt.I.unregister(), child: FilteredTasksScreen(), ), ); }, ), ], ), ], ), StatefulShellBranch( routes: [ GoRoute( path: '/rewards', name: RouteNames.rewards, redirect: loginRedirect, builder: (context, state) => RewardsScreen(), routes: [ GoRoute( path: 'new-reward', name: RouteNames.newReward, parentNavigatorKey: rootNavigatorKey, redirect: loginRedirect, pageBuilder: (context, state) { return CupertinoPage( fullscreenDialog: true, child: GoRouteWrapper( onInit: () { GetIt.I.registerSingleton( RewardEditingController(state.extra as Reward), ); }, onDispose: (context) { GetIt.I.unregister(); }, child: EditRewardScreen(), ), ); }, ), GoRoute( path: 'edit-reward', name: RouteNames.editReward, parentNavigatorKey: rootNavigatorKey, redirect: loginRedirect, pageBuilder: (context, state) { return CupertinoPage( fullscreenDialog: true, child: GoRouteWrapper( onInit: () { GetIt.I.registerSingleton( RewardEditingController(state.extra as Reward)..mode = RewardScreenMode.edit, ); }, onDispose: (context) { GetIt.I.unregister(); }, child: EditRewardScreen(), ), ); }, ), ]), ], ), StatefulShellBranch( routes: [ GoRoute( path: '/wallet', name: RouteNames.wallet, builder: (context, state) => WalletScreen(), routes: [ GoRoute( path: 'card-ready-banner-parent', name: RouteNames.walletCardReadyBannerParent, pageBuilder: (context, state) => CupertinoPage( fullscreenDialog: true, child: CardReadyParentFullscreenDialog(), ), ), GoRoute( path: 'card-ready-banner-child', name: RouteNames.walletCardReadyBannerChild, pageBuilder: (context, state) => CupertinoPage( fullscreenDialog: true, child: CardReadyChildFullscreenDialog(), ), ), GoRoute( path: 'top_up_bank_card', name: RouteNames.topUpBankCard, parentNavigatorKey: rootNavigatorKey, redirect: loginRedirect, builder: (context, state) => GoRouteWrapper( child: TopUpScreen(args: state.extra as TopUpParams?), onInit: () { GetIt.I.registerSingleton( TopUpController( walletRepository: GetIt.I(), userController: GetIt.I(), bankController: GetIt.I(), ), ); }, onDispose: (context) { GetIt.I.unregister(); }, ), ), GoRoute( path: 'binding-card-webview', name: RouteNames.bindingCardWebview, builder: (context, state) => GoRouteWrapper( child: CardBindingWebView(), onInit: () { GetIt.I.registerSingleton( BindingCardWebviewController( repository: GetIt.I(), userController: GetIt.I(), cardPurpose: state.extra as BindingCardPurpose, ), ); }, onDispose: (context) { GetIt.I.unregister(); }, ), ), GoRoute( path: 'add-or-remove-coins', name: RouteNames.addOrRemoveCoins, parentNavigatorKey: rootNavigatorKey, redirect: loginRedirect, pageBuilder: (context, state) => CupertinoPage( fullscreenDialog: true, child: AddOrRemoveCoinsScreen( mode: state.extra as ActionOnCoins, ), ), ), GoRoute( path: 'convert-coins-to-fiat', name: RouteNames.convertCoinsToFiat, parentNavigatorKey: rootNavigatorKey, redirect: loginRedirect, pageBuilder: (context, state) => CupertinoPage( fullscreenDialog: true, child: GoRouteWrapper( onInit: () { GetIt.I.registerSingleton( ConvertCoinsController( walletController: GetIt.I(), walletRepository: GetIt.I(), ), ); }, onDispose: (_) { GetIt.I.unregister(); }, child: ConvertCoinsToFiatScreen(), ), ), ), GoRoute( path: 'exchange-coins-requests', name: RouteNames.exchangeCoinsRequests, redirect: loginRedirect, pageBuilder: (context, state) => CupertinoPage( child: ExchangeCoinsRequestsScreen(), ), ), ], ), ], ), StatefulShellBranch( routes: [ GoRoute( path: '/profile', name: RouteNames.profile, redirect: loginRedirect, builder: (context, state) => ProfileScreen(), routes: [ GoRoute( path: 'edit', name: RouteNames.editProfile, pageBuilder: (context, state) => CupertinoPage( child: GoRouteWrapper( onInit: () { GetIt.I.registerSingleton( EditProfileController( userController: GetIt.I(), editableUser: state.extra as User, ), ); }, onDispose: (context) { GetIt.I.unregister(); }, child: EditProfileScreen(), ), ), ), GoRoute( path: 'edit-family', name: RouteNames.editFamily, redirect: loginRedirect, builder: (context, state) => EditFamilyScreen(), ), ], ), ], ), ], builder: (context, state, navigationShell) => GoRouteWrapper( onInit: () { final userController = GetIt.I(); final bankCardsRepository = GetIt.I.registerSingleton(BankCardsRepository()); // Bank cards final bankCardsController = GetIt.I.registerSingleton( BankCardsController( bankCardsRepository, userController, ), ); // Wallet final walletRepository = GetIt.I.registerSingleton(WalletRepository()); final walletController = GetIt.I.registerSingleton( WalletController( walletRepository: walletRepository, userController: userController, ), ); final exchangeRequestsController = GetIt.I.registerSingleton( ExchangeRequestsController(walletRepository: walletRepository), ); // Task final taskRepository = GetIt.I.registerSingleton(TaskRepository()); final taskController = GetIt.I.registerSingleton( TaskController( userController: userController, taskRepository: taskRepository, walletController: walletController, ), ); // Rewards final rewardsRepository = GetIt.I.registerSingleton(RewardsRepository()); final rewardsController = GetIt.I.registerSingleton(RewardsController( userController: userController, rewardsRepository: rewardsRepository, walletController: walletController, )); GetIt.I.registerSingleton( HomeController( userController: GetIt.I(), taskController: taskController, rewardsController: rewardsController, walletController: walletController, bankCardsController: bankCardsController, exchangeRequestsController: exchangeRequestsController, sharedPrefsInteractor: GetIt.I(), ), ); // Junior // GetIt.I.registerSingleton(JuniorController()); }, onDispose: (context) { GetIt.I.unregister(); // Wallet GetIt.I.unregister(); GetIt.I.unregister(); // Task GetIt.I.unregister(); GetIt.I.unregister(); // Rewards GetIt.I.unregister(); GetIt.I.unregister(); // Junior // GetIt.I.unregister(); // ChildCards GetIt.I.unregister(); GetIt.I.unregister(); GetIt.I.unregister(); }, child: HomeScreen(navigationShell: navigationShell), ), ), ], ); String getInitialRoute() { // если в настройках разработчика указано "Показывать экран разработчика на старте" - закидываем его в стек и ждем выхода пользователя if (GetIt.I().showAtStartup) { return '/dev'; } if (GetIt.I().isLoggedIn) { return '/tasks'; } else { return '/auth/login'; } } /// если юзер не залогинился, отправляем его на экран логина String? loginRedirect(BuildContext context, GoRouterState state) { if (GetIt.I().isLoggedIn == false) { return '/auth/login'; } else { return null; } } ```

I also edited the starting post and added a screenshot of the troublesome router config piece.

subzero911 commented 5 months ago

Everything worked fine until I tried to push /wallet/card-ready-banner-parent from /dev I expected that it just will spawn this screen on top of the root navigator, but it crashed instead.

sc-ot commented 5 months ago

What happens if you navigate with the navigation function from go_router?

context.goNamed(RouteNames. walletCardReadyBannerParent) to navigate from /dev to this route

Can you also try to add the rootNavigatorKey instead of a custom Key to your ShellBranch? But pass it as a parentNavigatorKey like you did with the others:

parentNavigatorKey: rootNavigatorKey,

subzero911 commented 5 months ago

context.goNamed(RouteNames. walletCardReadyBannerParent) to navigate from /dev to this route

It works as expected. It drops /dev, brings me to a /wallet tab, and a banner is opened.

Can you also try to add the rootNavigatorKey instead of a custom Key to your ShellBranch?

I'm not quite understand what do you mean? StatefulShellBranch does not have parentNavigatorKey parameter, but navigatorKey only. Putting rootNavigatorKey into inner Navigator makes no sense.

subzero911 commented 5 months ago

The workaround is to push it with the classic Navigator.push

image

But I'm pretty sure that it's a go_router's bug. I suppose that it tries to build a hierarchy: /dev/wallet/card-ready-banner-parent (to put wallet/card-ready-banner-parent on top of /dev) and can't solve it as /wallet/ is under StatefulShellRoute and /dev is outside. In this case it should fallback to simply pushing this screen on top of the navigator (like Navigator.push does), as it seems to me

sc-ot commented 5 months ago

I mean if you look in the docs https://docs.page/csells/go_router/navigation they use

onTap: () => GoRouter.of(context).go('/page2') or onTap: () => context.go('/page2')

or for named routes https://docs.page/csells/go_router/named-routes they use the goNamed:

context.goNamed('person', params: {'fid': fid, 'pid': pid});

I would stick to how it is done inside the docs, instead of using the classic Navigator

subzero911 commented 5 months ago

No-no-no, I don't want to use go / goNamed! I want to use push to push the "banner" screen above the current stack. I expect that when I do push /wallet/card-ready-banner-parent - it will push the screen lying on this path which corresponds to CardReadyParentFullscreenDialog(). It definitely shouldn't crash.

darshankawar commented 5 months ago

@subzero911 Can you take a look at https://github.com/flutter/flutter/issues/107045 and fix made to see if it helps in your case ? It does mention Navigator.push.

subzero911 commented 5 months ago

I checked this issue, they applied the same workaround - Navigator.push instead of context.push. It didn't resolve the issue.

subzero911 commented 5 months ago

I found yet another strange behaviour.

Previously, my setup was the following: I'm at the ShellRoute, I go to Profile tab (branch) and call context.push('/dev'). Then I call context.push(RouteNames.walletCardReadyBannerChild) and get a "!keyReservation.contains(key)" bug.

Now I tried to do context.go('/dev') to drop the history. Then I called context.push(RouteNames.walletCardReadyBannerChild) and got an error "No initial matches: walletCardReadyBannerChild" and "Page not found" screen. So, if aGoRoute declared in a StatefulShellBranch, it can't be pushed independently. Maybe it will help somehow.

darshankawar commented 5 months ago

Thanks for the update. Keeping it open for team's tracking.

pr0xyMity commented 1 week ago

Is this topic resolved? I have same issues