{"id":27217,"date":"2024-03-21T05:00:00","date_gmt":"2024-03-21T04:00:00","guid":{"rendered":"https:\/\/sii.pl\/blog\/?p=27217"},"modified":"2024-07-22T14:11:18","modified_gmt":"2024-07-22T12:11:18","slug":"migration-to-go_router-devs-story","status":"publish","type":"post","link":"https:\/\/sii.pl\/blog\/en\/migration-to-go_router-devs-story\/","title":{"rendered":"Migration to go_router\u200a\u2014\u200adev\u2019s\u00a0story"},"content":{"rendered":"\n<p>Our client always wanted to have deeplinks provided by push notifications. We developed navigation in the client\u2019s app using Navigator\u2019s methods, such as pop\/push\/pushReplacement, etc. We even created simple deeplink handling with a\u00a0.popUntil() solution that allowed us to open specific tabs from Bottom Navigation. But we decided to wait until Google chooses one of 3 libraries to help devs implement Navigator 2.0, which supports deeplinks.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image1-3.png\"><img decoding=\"async\" width=\"800\" height=\"116\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image1-3.png\" alt=\"go_router 7.1.1\" class=\"wp-image-27218\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image1-3.png 800w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image1-3-300x44.png 300w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image1-3-768x111.png 768w\" sizes=\"(max-width: 800px) 100vw, 800px\" \/><\/a><figcaption class=\"wp-element-caption\">Fig. 1 go_router 7.1.1<\/figcaption><\/figure>\n\n\n\n<p>At the end of 2021, Google chose 3 libraries to support Navigator 2.0: beamer, vroute, and autoroute, and did deep research on <a href=\"https:\/\/github.com\/flutter\/uxr\/tree\/master\/nav2-usability\/comparative-analysis\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >which one should be supported<\/a>.<\/p>\n\n\n\n<p>But then suddenly, they changed their mind and took go_router as an officially supported package. Many disappointed devs even created some Reddit threads to discuss this subject. So, should you migrate to go_router? <\/p>\n\n\n\n<p>I will not answer that question, but <strong>I will try to show you why we decided to do that<\/strong> and how we handled cases where go_router made us trouble. Let\u2019s start!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Basics\/Research<\/strong><\/h2>\n\n\n\n<p>Like always, before doing anything, we decided to do some research. Our Architect asked go_router\u2019s team directly about our app use cases, and we decided to wait until go_router 5.0 was available, which would solve all basic problems. So, we started with version 5.0.0, but before we added any code to the client app, we developed a PoC project to test all cases to see if they could handle.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Deeplink support<\/h2>\n\n\n\n<p>go_router supports deeplinks out of the box; we should only enable them on Android\/iOS <a href=\"https:\/\/docs.flutter.dev\/ui\/navigation\/deep-linking\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >following the instructions<\/a>. For Android, we can test deeplinks using adb commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nadb shell &#039;am start -a android.intent.action.VIEW I will schedule some time for us to connect.\n    -c android.intent.category.BROWSABLE Thank you for reaching out.\n    -d &quot;http:\/\/&amp;lt;web-domain&gt;\/details&quot;&#039; I will schedule some time for us to connect.\n    &amp;lt;package name&gt;\n<\/pre><\/div>\n\n\n<p><strong>Note!<\/strong> There are some problems with passing more than one argument to deeplinks on the Windows terminal because of encoding. To avoid this issue, you can use f.e. Git Bash terminal. Or using any supportive apps <a href=\"https:\/\/play.google.com\/store\/apps\/details?id=com.manoj.dlt&amp;hl=en&amp;gl=US\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >from Google Play<\/a>. For iOS we just used build-in notepad with pasted deeplinks.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Nested bottom navigation<\/strong><\/h2>\n\n\n\n<p>We had to rewrite our bottom navigation from scratch based on the example <a href=\"https:\/\/github.com\/flutter\/packages\/blob\/main\/packages\/go_router\/example\/lib\/shell_route.dart\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >provided by GitHub<\/a>.\u00a0<br><br>It uses ShellRoute to create a new Navigator, which displays any matching sub-routes instead of placing them on the root Navigator.<br><strong>However, a<\/strong> few days ago, the go_router team published a new version of go_router v.7.1.0, which supports StatefulShellRoute.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img decoding=\"async\" width=\"800\" height=\"154\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image2-2.png\" alt=\"StatefulShellRoute\" class=\"wp-image-27222\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image2-2.png 800w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image2-2-300x58.png 300w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image2-2-768x148.png 768w\" sizes=\"(max-width: 800px) 100vw, 800px\" \/><figcaption class=\"wp-element-caption\">Fig. 2 StatefulShellRoute<\/figcaption><\/figure>\n\n\n\n<p>This is a game-changer for preserving the state in the app\u2019s navigation. Many people have been <a href=\"https:\/\/github.com\/flutter\/flutter\/issues\/99124\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >waiting for this for a long time<\/a>.<\/p>\n\n\n\n<p>It is easy to use:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nfinal GoRouter _router = GoRouter(\n    navigatorKey: _rootNavigatorKey,\n    initialLocation: &#039;\/a&#039;,\n    routes: &amp;lt;RouteBase&gt;&#x5B;\n      StatefulShellRoute(\n        builder: (BuildContext context, GoRouterState state,\n            StatefulNavigationShell navigationShell) {\n          \/\/ This nested StatefulShellRoute demonstrates the use of a\n          \/\/ custom container for the branch Navigators. In this implementation,\n          \/\/ no customization is done in the builder function (navigationShell\n          \/\/ itself is simply used as the Widget for the route). Instead, the\n          \/\/ navigatorContainerBuilder function below is provided to\n          \/\/ customize the container for the branch Navigators.\n          return navigationShell;\n        },\n        navigatorContainerBuilder: (BuildContext context,\n            StatefulNavigationShell navigationShell, List&amp;lt;Widget&gt; children) {\n          \/\/ Returning a customized container for the branch\n          \/\/ Navigators (i.e. the `List&amp;lt;Widget&gt; children` argument).\n          \/\/\n          \/\/ See ScaffoldWithNavBar for more details on how the children\n          \/\/ are managed (using AnimatedBranchContainer).\n          return ScaffoldWithNavBar(\n              navigationShell: navigationShell, children: children);\n        },\n        branches: &amp;lt;StatefulShellBranch&gt;&#x5B;\n          \/\/ The route branch for the first tab of the bottom navigation bar.\n          StatefulShellBranch(\n            navigatorKey: _tabANavigatorKey,\n            routes: &amp;lt;RouteBase&gt;&#x5B;\n              GoRoute(\n                \/\/ The screen to display as the root in the first tab of the\n                \/\/ bottom navigation bar.\n                path: &#039;\/a&#039;,\n                builder: (BuildContext context, GoRouterState state) =&gt;\n                    const RootScreenA(),\n                routes: &amp;lt;RouteBase&gt;&#x5B;\n                  \/\/ The details screen to display stacked on navigator of the\n                  \/\/ first tab. This will cover screen A but not the application\n                  \/\/ shell (bottom navigation bar).\n                  GoRoute(\n                    path: &#039;details&#039;,\n                    builder: (BuildContext context, GoRouterState state) =&gt;\n                        const DetailsScreen(label: &#039;A&#039;),\n                  ),\n                ],\n              ),\n            ],\n          ),\n\n          \/\/ The route branch for the third tab of the bottom navigation bar.\n          StatefulShellBranch(\n            \/\/ StatefulShellBranch will automatically use the first descendant\n            \/\/ GoRoute as the initial location of the branch. If another route\n            \/\/ is desired, specify the location of it using the defaultLocation\n            \/\/ parameter.\n            \/\/ defaultLocation: &#039;\/c2&#039;,\n            routes: &amp;lt;RouteBase&gt;&#x5B;\n              StatefulShellRoute(\n                builder: (BuildContext context, GoRouterState state,\n                    StatefulNavigationShell navigationShell) {\n                  \/\/ Just like with the top level StatefulShellRoute, no\n                  \/\/ customization is done in the builder function.\n                  return navigationShell;\n                },\n                navigatorContainerBuilder: (BuildContext context,\n                    StatefulNavigationShell navigationShell,\n                    List&amp;lt;Widget&gt; children) {\n                  \/\/ Returning a customized container for the branch\n                  \/\/ Navigators (i.e. the `List&amp;lt;Widget&gt; children` argument).\n                  \/\/\n                  \/\/ See TabbedRootScreen for more details on how the children\n                  \/\/ are managed (in a TabBarView).\n                  return TabbedRootScreen(\n                      navigationShell: navigationShell, children: children);\n                },\n                \/\/ This bottom tab uses a nested shell, wrapping sub routes in a\n                \/\/ top TabBar.\n                branches: &amp;lt;StatefulShellBranch&gt;&#x5B;\n                  StatefulShellBranch(routes: &amp;lt;GoRoute&gt;&#x5B;\n                    GoRoute(\n                      path: &#039;\/b1&#039;,\n                      builder: (BuildContext context, GoRouterState state) =&gt;\n                          const TabScreen(\n                              label: &#039;B1&#039;, detailsPath: &#039;\/b1\/details&#039;),\n                      routes: &amp;lt;RouteBase&gt;&#x5B;\n                        GoRoute(\n                          path: &#039;details&#039;,\n                          builder:\n                              (BuildContext context, GoRouterState state) =&gt;\n                                  const DetailsScreen(\n                            label: &#039;B1&#039;,\n                            withScaffold: false,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ]),\n                  StatefulShellBranch(routes: &amp;lt;GoRoute&gt;&#x5B;\n                    GoRoute(\n                      path: &#039;\/b2&#039;,\n                      builder: (BuildContext context, GoRouterState state) =&gt;\n                          const TabScreen(\n                              label: &#039;B2&#039;, detailsPath: &#039;\/b2\/details&#039;),\n                      routes: &amp;lt;RouteBase&gt;&#x5B;\n                        GoRoute(\n                          path: &#039;details&#039;,\n                          builder:\n                              (BuildContext context, GoRouterState state) =&gt;\n                                  const DetailsScreen(\n                            label: &#039;B2&#039;,\n                            withScaffold: false,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ]),\n                ],\n              ),\n            ],\n          ),\n        ],\n      ),\n    ],\n  );\n  \/\/\/ Navigate to the current location of the branch at the provided index when\n  \/\/\/ tapping an item in the BottomNavigationBar.\n  void _onTap(BuildContext context, int index) {\n    \/\/ When navigating to a new branch, it&#039;s recommended to use the goBranch\n    \/\/ method, as doing so makes sure the last navigation state of the\n    \/\/ Navigator for the branch is restored.\n    navigationShell.goBranch(\n      index,\n      \/\/ A common pattern when using bottom navigation bars is to support\n      \/\/ navigating to the initial location when tapping the item that is\n      \/\/ already active. This example demonstrates how to support this behavior,\n      \/\/ using the initialLocation parameter of goBranch.\n      initialLocation: index == navigationShell.currentIndex,\n    );\n  }\n}\n<\/pre><\/div>\n\n\n<p>And works well:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image3.gif\"><img decoding=\"async\" width=\"356\" height=\"772\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image3.gif\" alt=\"the result\" class=\"wp-image-27225\"\/><\/a><figcaption class=\"wp-element-caption\">Fig. 3 The result<\/figcaption><\/figure>\n\n\n\n<p>We haven\u2019t migrated to it yet, but it will be an improvement in the future.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>\u201clogin\u201d guard<\/strong><\/h3>\n\n\n\n<p>Previously, we researched a beamer package that won Google\u2019s routing competition. They have a nice feature called <em>guards<\/em>:<\/p>\n\n\n\n<p><a href=\"https:\/\/pub.dev\/packages\/beamer#guards\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" ><strong>beamer | Flutter Package<\/strong><br><em>beamer.dev Beamer uses the power of Router and implements all the underlying logic for you, letting you explore\u2026<\/em>pub.dev<\/a><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nBeamGuard(\n  \/\/ on which path patterns (from incoming routes) to perform the check\n  pathPatterns: &#x5B;&#039;\/login&#039;],\n  \/\/ perform the check on all patterns that **don&#039;t** have a match in pathPatterns\n  guardNonMatching: true,\n  \/\/ return false to redirect\n  check: (context, location) =&gt; context.isUserAuthenticated(),\n  \/\/ where to redirect on a false check\n  beamToNamed: (origin, target) =&gt; &#039;\/login&#039;,\n)\n<\/pre><\/div>\n\n\n<p>In short, guards will protect access to your app&#8217;s pages if, for example, a user is not signed in or signed out. In our case, we needed 2 different types of \u201cguards\u201d in our app. The equivalent of it is called redirect in go_rotuer:<\/p>\n\n\n\n<p><a href=\"https:\/\/pub.dev\/documentation\/go_router\/latest\/topics\/Redirection-topic.html\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" ><strong>Redirection topic<\/strong><br><em>Redirection changes the location to a new one based on the application state. For example, redirection can be used to\u2026<\/em>pub.dev<\/a><\/p>\n\n\n\n<p>To add it, we used 2 parameters:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>refreshListenable, which is an observed stream and calls<\/li>\n\n\n\n<li>redirect when anything is emitted from that stream.<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nrefreshListenable: GoRouterRefreshStream(\n    GetIt.I&amp;lt;AuthorizationService&gt;().authStream,\n  ),\n  redirect: (context, state) async {\n    final loggedIn = context.isLoggedIn;\n    final loginLocation = state.namedLocation(loginRouteName);\n\n    if (!loggedIn) {\n      return Uri(\n        path: loginLocation,\n        queryParameters: {&#039;deeplink&#039;: state.location},\n      ).toString();\n    }\n    return null;\n  },\n);\n<\/pre><\/div>\n\n\n<p><strong>Note! <\/strong>refreshListenable requires an object called Listenable, which is not easy to create from a Stream, but the go_router team created a class:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nclass GoRouterRefreshStream extends ChangeNotifier {\n  GoRouterRefreshStream(Stream&amp;lt;dynamic&gt; stream) {\n    notifyListeners();\n    _subscription = stream.asBroadcastStream().listen(\n          (dynamic _) =&gt; notifyListeners(),\n        );\n  }\n\n  late final StreamSubscription&amp;lt;dynamic&gt; _subscription;\n\n  @override\n  void dispose() {\n    _subscription.cancel();\n    super.dispose();\n  }\n}\n<\/pre><\/div>\n\n\n<p>Which was removed in one of <a href=\"https:\/\/github.com\/flutter\/flutter\/issues\/108128\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >the earlier versions<\/a>.<br>However, I found nothing we can easily use from Flutter SDK, so I just copied and used this class. It works just fine.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Assumptions<\/strong><\/h3>\n\n\n\n<p>Before we started migration, we agreed to make some assumptions:<\/p>\n\n\n\n<ol class=\"wp-block-list\" start=\"1\">\n<li>We agreed to have One Source Of Truth as the go_router state, which will manipulate the app state via go_router methods like context.goNamed()<\/li>\n\n\n\n<li>We will cover migration behind feature flags so as not to break production code and old navigation.<\/li>\n\n\n\n<li>We will support only deeplinks with our custom schema.<\/li>\n\n\n\n<li>When the main migration (main screens and features) is done, every new PR that touches navigation must include navigation for go_router.<\/li>\n\n\n\n<li>We will not make workarounds for problems added to issues; we will just wait until the solution is implemented in the next version of go_rotuer (for example, returning value for\u00a0.pop method).<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Migration<\/strong><\/h3>\n\n\n\n<p>To make migration possible, we created a new \u201cMyApp\u201d main widget with a configured router covered by a feature flag:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nrunApp(\n goRouter.isEnabled ? RouterApp() : MyApp(),\n);\n<\/pre><\/div>\n\n\n<p>From now we will reuse all screens and widgets by adding to them routes, like:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nconst myScreenRouteName = &#039;MyScreenRoute&#039;;\nconst myScreenPathName = &#039;\/myScreen&#039;;\n\nfinal myScreenRoute = GoRoute(\n  parentNavigatorKey: shellNavigatorKey,\n  path: myScreenPathName,\n  name: myScreenRouteName,\n  builder: (context, state) {\n    (...)\n    return const MyScreen();\n  },\n);\n<\/pre><\/div>\n\n\n<p>We also documented all flows using go_router using markdown flows:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img decoding=\"async\" width=\"220\" height=\"435\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image4-1.png\" alt=\"The flows\" class=\"wp-image-27228\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image4-1.png 220w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image4-1-152x300.png 152w\" sizes=\"(max-width: 220px) 100vw, 220px\" \/><figcaption class=\"wp-element-caption\">Fig. 4 The flows<\/figcaption><\/figure>\n\n\n\n<p>We started with all places where old navigator methods were used, like\u00a0.pop,\u00a0.push,\u00a0.popUntil, etc., by adding routes and new method calls.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Deeplink support<\/strong><\/h3>\n\n\n\n<p>By default, go_router supports deeplinks out of the box; however, in our case, we needed to make them more custom. We don\u2019t have a one-screen login process, so we had to \u201csave\u201d the deeplink and \u201cuse\u201d immediately. To do that, we added a top-based (provided) cubit called DeeplinkCubit\u00a0:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport &#039;package:flutter_bloc\/flutter_bloc.dart&#039;;\n\nclass DeeplinkCubit extends Cubit&amp;lt;String&gt; {\n  DeeplinkCubit(super.initialState);\n\n  void addDeeplink(String deeplink) {\n    emit(deeplink);\n  }\n}\n<\/pre><\/div>\n\n\n<p>And when we receive a deeplink on the app state and loginguard doesn\u2019t allow us to use it (redirects to Login Screen), we have to store it by:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ncontext.read&amp;lt;DeeplinkCubit&gt;().addDeeplink(&#039;deeplink&#039;);\n<\/pre><\/div>\n\n\n<p>And read as:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ncontext.read&amp;lt;DeeplinkCubit&gt;().state;\n<\/pre><\/div>\n\n\n<p>To make it easier, we added helper methods as extensions like:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nextension Deeplink on GoRouterState {\n  String? get deeplink =&gt; queryParameters&#x5B;&#039;deeplink&#039;];\n}\n\nextension GoRouterContext on BuildContext {\n  GoRouterState get goRouterState =&gt; GoRouterState.of(this);\n\n  GoRouter get router =&gt; GoRouter.of(this);\n\n  DeeplinkCubit get deeplinkCubit =&gt; BlocProvider.of&amp;lt;DeeplinkCubit&gt;(this);\n\n  void deeplinkOrGoNamed(\n    String name, {\n    Map&amp;lt;String, String&gt; params = const {},\n    Map&amp;lt;String, dynamic&gt; queryParams = const {},\n    Object? extra,\n  }) {\n    final deeplink = deeplinkCubit.state;\n    if (deeplink.isNotEmpty) {\n      deeplinkCubit.addDeeplink(null);\n      go(deeplink, extra: extra);\n    } else {\n      goNamed(\n        name,\n        pathParameters: params,\n        queryParameters: queryParams,\n        extra: extra,\n      );\n    }\n  }\n\n  void goNamedWithDeeplink(\n    String name, {\n    Map&amp;lt;String, String&gt; params = const {},\n    Map&amp;lt;String, dynamic&gt;? queryParams,\n    Object? extra,\n  }) {\n    final deeplink = goRouterState.deeplink;\n    if (deeplink != null) {\n      deeplinkCubit.addDeeplink(deeplink);\n    }\n    queryParams ??= {};\n    if (isNotBlank(deeplink)) {\n      queryParams.addAll({&#039;deepLink&#039;: deeplink});\n    }\n    goNamed(\n      name,\n      pathParameters: params,\n      queryParameters: queryParams,\n      extra: extra,\n    );\n  }\n}\n<\/pre><\/div>\n\n\n<p>From now on, we can use goNamedWithDeeplink to store deeplink until the login process is done, and deeplinkOrGoNamed will be used when the user is redirected from LoginScreen to any loggedInScope content.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Troubleshooting<\/strong><\/h3>\n\n\n\n<p>We ran into issues related to previous solutions and new approaches during the migration. We found solutions or workarounds that we would like to share.<\/p>\n\n\n\n<ol class=\"wp-block-list\" start=\"1\">\n<li><strong><em>Scoping in the app<\/em><\/strong> \u2013 we divided app sections into 2 scopes for signed-in users and signed-out by adding routes in the right order.<\/li>\n\n\n\n<li><strong><em>Typesafety<\/em> <\/strong>\u2013 go_rotuer supports typedsafety routes:<\/li>\n<\/ol>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport &#039;package:go_router\/go_router.dart&#039;;\n\npart &#039;go_router_builder.g.dart&#039;;\n\n@TypedGoRoute&amp;lt;HomeScreenRoute&gt;(\n    path: &#039;\/&#039;,\n    routes: &#x5B;\n      TypedGoRoute&amp;lt;SongRoute&gt;(\n        path: &#039;song\/:id&#039;,\n      )\n    ]\n)\n@immutable\nclass HomeScreenRoute extends GoRouteData {\n  @override\n  Widget build(BuildContext context) {\n    return const HomeScreen();\n  }\n}\n\n@immutable\nclass SongRoute extends GoRouteData {\n  final int id;\n\n  const SongRoute({\n    required this.id,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return SongScreen(songId: id.toString());\n  }\n}\n<\/pre><\/div>\n\n\n<p>and usage<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nTextButton(\n  onPressed: () {\n    const SongRoute(id: 2).go(context);\n  },\n  child: const Text(&#039;Go to song 2&#039;),\n),\n<\/pre><\/div>\n\n\n<p><a href=\"https:\/\/pub.dev\/documentation\/go_router\/latest\/topics\/Type-safe%20routes-topic.html\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" ><strong>Type-safe routes topic<\/strong><br><em>Instead of using URL strings to navigate, go_router supports type-safe routes using the go_router_builder package.<\/em>pub.dev<\/a><\/p>\n\n\n\n<p>Although it has some limitations, discussed on mentioned earlier reddit topic:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image5-1.png\"><img decoding=\"async\" width=\"800\" height=\"844\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image5-1.png\" alt=\"Reddit topic\" class=\"wp-image-27231\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image5-1.png 800w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image5-1-284x300.png 284w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image5-1-768x810.png 768w\" sizes=\"(max-width: 800px) 100vw, 800px\" \/><\/a><figcaption class=\"wp-element-caption\">Fig. 5 Reddit topic<\/figcaption><\/figure>\n\n\n\n<p>However, we needed to pass more than Strings\/ints to routes. So we agreed to pass to extras \u201cdata classes\u201d objects when needed to pass more than one object via extras:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ncontext.pushNamed(\n    bookstoreScreenRouteName,\n    extra: BookContent(\n      bookId: cubit.bookId,\n      shortDesc: shortDesc,\n    ),\n);\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\"><strong>Proper handling of returning value from\u00a0.pop(true\/false)<\/strong><\/h2>\n\n\n\n<p>We had a few places to return value after using\u00a0the .pop(value) method. Until go_router v 6.5.0, it was not possible to do; even workarounds were very difficult to apply. So we decided to wait for when go_router team to add support for it, and they did:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img decoding=\"async\" width=\"800\" height=\"106\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image6-2.png\" alt=\"Supports returning values on pop\" class=\"wp-image-27233\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image6-2.png 800w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image6-2-300x40.png 300w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/image6-2-768x102.png 768w\" sizes=\"(max-width: 800px) 100vw, 800px\" \/><figcaption class=\"wp-element-caption\">Fig. 6 Supports returning values on pop<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>There is a problem with using BlocProvider in subroutes and get it<\/strong><\/h2>\n\n\n\n<p>It returns an error:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Bad state: No parent widget of type ... in the widget tree',<\/pre>\n\n\n\n<p>It is because it creates a new tree without providing a bloc in context. The current solution is to pass BLoC to the routes in extras (<a href=\"https:\/\/stackoverflow.com\/questions\/73956881\/how-to-pass-a-bloc-to-another-screen-using-go-router-on-flutter\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >more information<\/a>).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Migration<\/strong><\/h2>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nNavigator.of(context).popUntil((route) =&gt; route.isFirst);\n<\/pre><\/div>\n\n\n<p>We can just use goNamed because it will build a path from scratch.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Equivalent for<\/strong><\/h2>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nNavigator.of(this).push(\n  CupertinoPageRoute(\n    builder: (_) =&gt; destination),\n  );\n}\n<\/pre><\/div>\n\n\n<p>Instead, we can use just push\/go with a page builder like:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npageBuilder: (context, state) {\n  return CupertinoPage(\n    child: const MyOtherScreen(),\n  );\n},\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\"><strong>Notifications\/push deeplink, which will wait for fetching data<\/strong><\/h2>\n\n\n\n<p>The problem here might be described as the User receiving a notification with a deeplink to some details page, but to do that, the app has to fetch some data and open the details screen after that. To make it happen, I used the previously shown DeeplinkCubit to store deeplink values from notifications. The app goes to the proper screen by the goNamed method, and when the state changes to the state fetched in BlocListener, we added the method:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nbool _shouldHandleDeeplink(String deeplink) =&gt; (deeplink.isNotEmpty);\n\n  void handleDeeplink(\n    BuildContext context,\n    List&amp;lt;Data&gt;? data\n  ) {\n    final myObject = list?.firstWhereOrNull(\n      (item) =&gt; item.itemId == data.itemId,\n    );\n    if (myObject != null) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        context.deeplinkBloc.addDeeplink(&#039;&#039;);\n        context.pushNamed(\n          myDetailsScreen,\n          extra: DataDetails(item: myObject),\n        );\n      });\n    }\n  }\n}\n<\/pre><\/div>\n\n\n<p>When data is fetched we check if we stored deeplink by _shouldHandleDeeplink method and then navigate to details screen.<\/p>\n\n\n\n<p><strong>Note! <\/strong>We added `WidgetsBinding.instance.addPostFrameCallback` to wait until Flutter builds a widget and then use context to change a state. Otherwise, you will receive an exception.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>OpenContainer<\/strong><\/h2>\n\n\n\n<p>OpenContainer does not work well with go_router. It is possible now to pass route settings to it:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n\/\/\/ Provides additional data to the &#x5B;openBuilder] route the Navigator pushes.\nFinal RouteSettings? routeSettings;\n<\/pre><\/div>\n\n\n<p>But in our case, there were some problems with it. So we decided to get rid of it and use custom animation instead:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nTween&amp;lt;RelativeRect&gt; createTween(BuildContext context) {\n  final windowSize = MediaQuery.of(context).size;\n  final box = context.findRenderObject() as RenderBox;\n  final rect = box.localToGlobal(Offset.zero) &amp;amp; box.size;\n  final relativeRect = RelativeRect.fromSize(rect, windowSize);\n\n  return RelativeRectTween(\n    begin: relativeRect,\n    end: RelativeRect.fill,\n  );\n}\n<\/pre><\/div>\n\n\n<p>And use it as transitionBuilder parameter to make custom transitions:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nfinal myScreenRoute = GoRoute(\n  path: &#039;myScreen&#039;,\n  name: myScreenRouteName,\n  pageBuilder: (context, state) {\n    final data = state.extra as MyScreenData;\n    return CustomTransitionPage&amp;lt;void&gt;(\n      key: state.pageKey,\n      child: _MyScreen(\n        data: data.data,\n      ),\n      transitionsBuilder: (context, animation, secondaryAnimation, child) {\n        final rectAnimation =\n            data.tween.chain(CurveTween(curve: Curves.ease)).animate(animation);\n        return Stack(\n          children: &#x5B;\n            PositionedTransition(rect: rectAnimation, child: child),\n          ],\n        );\n      },\n    );\n  },\n);\n<\/pre><\/div>\n\n\n<p><strong>Note! <\/strong>It is not 1:1 equivalent, but similar enough to be accepted. There might be further need for development to tweak it with some animation combinations.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>go_router parent builders<\/strong><\/h2>\n\n\n\n<p>If you have routes like:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nschema:\/\/com.example.name\/ScreenA\n<\/pre><\/div>\n\n\n<p>and want to go deeper to:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nschema:\/\/com.example.name\/ScreenA\/detailsA page\n<\/pre><\/div>\n\n\n<p>you must remember that when you call push\/go for details<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nGoRouter.of(context).go(&#039;\/a\/details&#039;);\n<\/pre><\/div>\n\n\n<p>the builder from ScreenA (parent) also will be called:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#x5B;GoRouter] setting initial location \/a\nI\/flutter (20958): A builder\n&#x5B;GoRouter] Using MaterialApp configuration\n&#x5B;GoRouter] going to \/a\/details\nI\/flutter (20958): A details builder\nI\/flutter (20958): A builder\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\"><strong>Summary\u200a\u2014\u200aare we\/client happy?<\/strong><\/h2>\n\n\n\n<p>I presented a demo of our results to the client, mainly showcasing the deeplink support, and he\/she\/they were happy&nbsp;\ud83d\ude42 It was more challenging to develop\/migrate than initially anticipated, but we managed to accomplish it. Occasionally, our QA team may still encounter bugs, whether directly related to implementing the new approach using go_router or not.<\/p>\n\n\n\n<p>Still, we now have a better understanding of how to handle them. We invested a significant amount of time in searching for solutions to our problems, and to save you time, this article has been written. I hope someone finds it useful.<\/p>\n\n\n\n<p>In my opinion, go_router is continuously improving, similar to Dart compared to Kotlin or Swift. I believe it is easier to start an app from scratch, but migration is still achievable if that\u2019s not possible.<\/p>\n\n\n\n<p>If you have any questions or suggestions, please leave a comment below.<\/p>\n\n\n\n<p>Thank you for reading.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Helpful links<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/pub.dev\/documentation\/go_router\/latest\/go_router\/ShellRoute-class.html\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >ShellRoute class<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/gist.github.com\/bizz84\/911b984e30b16bee8cb090de98ab68f2\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >Go_router_nested_navigation.dart<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/codewithandrea.com\/articles\/flutter-bottom-navigation-bar-nested-routes-gorouter\/\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >Flutter the Bottom Navigation Bar with Stateful Nested Routes using GoRouter<\/a><\/li>\n<\/ul>\n\n\n<div class=\"kk-star-ratings kksr-auto kksr-align-left kksr-valign-bottom\"\n    data-payload='{&quot;align&quot;:&quot;left&quot;,&quot;id&quot;:&quot;27217&quot;,&quot;slug&quot;:&quot;default&quot;,&quot;valign&quot;:&quot;bottom&quot;,&quot;ignore&quot;:&quot;&quot;,&quot;reference&quot;:&quot;auto&quot;,&quot;class&quot;:&quot;&quot;,&quot;count&quot;:&quot;1&quot;,&quot;legendonly&quot;:&quot;&quot;,&quot;readonly&quot;:&quot;&quot;,&quot;score&quot;:&quot;5&quot;,&quot;starsonly&quot;:&quot;&quot;,&quot;best&quot;:&quot;5&quot;,&quot;gap&quot;:&quot;11&quot;,&quot;greet&quot;:&quot;&quot;,&quot;legend&quot;:&quot;5\\\/5 ( vote: 1)&quot;,&quot;size&quot;:&quot;18&quot;,&quot;title&quot;:&quot;Migration to go_router\u200a\u2014\u200adev\u2019s\u00a0story&quot;,&quot;width&quot;:&quot;139.5&quot;,&quot;_legend&quot;:&quot;{score}\\\/{best} ( {votes}: {count})&quot;,&quot;font_factor&quot;:&quot;1.25&quot;}'>\n            \n<div class=\"kksr-stars\">\n    \n<div class=\"kksr-stars-inactive\">\n            <div class=\"kksr-star\" data-star=\"1\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"2\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"3\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"4\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"5\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n    <\/div>\n    \n<div class=\"kksr-stars-active\" style=\"width: 139.5px;\">\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n    <\/div>\n<\/div>\n                \n\n<div class=\"kksr-legend\" style=\"font-size: 14.4px;\">\n            5\/5 ( vote: 1)    <\/div>\n    <\/div>\n","protected":false},"excerpt":{"rendered":"<p>Our client always wanted to have deeplinks provided by push notifications. We developed navigation in the client\u2019s app using Navigator\u2019s &hellip; <a class=\"continued-btn\" href=\"https:\/\/sii.pl\/blog\/en\/migration-to-go_router-devs-story\/\">Continued<\/a><\/p>\n","protected":false},"author":621,"featured_media":27238,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_editorskit_title_hidden":false,"_editorskit_reading_time":0,"_editorskit_is_block_options_detached":false,"_editorskit_block_options_position":"{}","inline_featured_image":false,"footnotes":""},"categories":[1320],"tags":[2428,2183,1683,1604,1590],"class_list":["post-27217","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-hard-development","tag-digital-2","tag-go_router","tag-applications","tag-pros-and-cons","tag-tools"],"acf":[],"aioseo_notices":[],"republish_history":[],"featured_media_url":"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/03\/Migration-to-go_router\u200a\u2014\u200adevs-story.jpg","category_names":["Hard development"],"_links":{"self":[{"href":"https:\/\/sii.pl\/blog\/en\/wp-json\/wp\/v2\/posts\/27217"}],"collection":[{"href":"https:\/\/sii.pl\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sii.pl\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/en\/wp-json\/wp\/v2\/users\/621"}],"replies":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/en\/wp-json\/wp\/v2\/comments?post=27217"}],"version-history":[{"count":3,"href":"https:\/\/sii.pl\/blog\/en\/wp-json\/wp\/v2\/posts\/27217\/revisions"}],"predecessor-version":[{"id":27237,"href":"https:\/\/sii.pl\/blog\/en\/wp-json\/wp\/v2\/posts\/27217\/revisions\/27237"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/en\/wp-json\/wp\/v2\/media\/27238"}],"wp:attachment":[{"href":"https:\/\/sii.pl\/blog\/en\/wp-json\/wp\/v2\/media?parent=27217"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sii.pl\/blog\/en\/wp-json\/wp\/v2\/categories?post=27217"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sii.pl\/blog\/en\/wp-json\/wp\/v2\/tags?post=27217"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}