<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ruben Lopez's Flutter Blog]]></title><description><![CDATA[I created this blog to share my experiences with the community, improve my Flutter skills, and hopefully help others along the way.]]></description><link>https://blog.rubenlop88.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1737749966673/c1c5b2b6-c95b-4e3a-aac7-c60dfefcfb7b.png</url><title>Ruben Lopez&apos;s Flutter Blog</title><link>https://blog.rubenlop88.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 17 Apr 2026 11:41:32 GMT</lastBuildDate><atom:link href="https://blog.rubenlop88.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to implement popUntil in GoRouter 14.1.0]]></title><description><![CDATA[GoRouter is the official routing library recommended by the Flutter team. The goal of GoRouter is to make it easier to implement declarative navigation, as stated in the official docs. Like us at Cuballama, many projects that where using imperative n...]]></description><link>https://blog.rubenlop88.dev/how-to-implement-popuntil-in-gorouter-1410</link><guid isPermaLink="true">https://blog.rubenlop88.dev/how-to-implement-popuntil-in-gorouter-1410</guid><category><![CDATA[go_router]]></category><dc:creator><![CDATA[Ruben Lopez]]></dc:creator><pubDate>Thu, 03 Jul 2025 14:22:31 GMT</pubDate><content:encoded><![CDATA[<p>GoRouter is the official routing library recommended by the Flutter team. The goal of GoRouter is to make it easier to implement declarative navigation, as stated in the official <a target="_blank" href="https://docs.flutter.dev/ui/navigation#using-the-router">docs</a>. Like us at <a target="_blank" href="https://www.cuballama.com/">Cuballama</a>, many projects that where using imperative navigation (<code>pop</code>, <code>push</code>, <code>popUntil</code>), decided to migrate to GoRouter, in part because of this recommendation. But there was a piece missing, GoRouter does not have a <code>popUntil</code> method. There is an open <a target="_blank" href="https://github.com/flutter/flutter/issues/131625">issue</a> about it, but it is very unlikely that it will ever be implemented because GoRouter is now considered "feature complete."</p>
<p>That’s why in our code we kept using <a target="_blank" href="https://api.flutter.dev/flutter/widgets/Navigator/popUntil.html">Navigator.popUntil</a> when needed, but starting with GoRouter 14.1.0, it was causing an infinite loop and crashing the app. A lot of people also implemented their own versions of <code>popUntil</code>, like the one shown in this <a target="_blank" href="https://medium.com/@ngocmanh1609/designing-gorouters-routes-for-popuntil-feature-in-flutter-480d66e4c356">post</a>, but all of them started failing too, as reported in this <a target="_blank" href="https://github.com/flutter/flutter/issues/148185">issue</a>. Furthermore, the problem only seemed to appear when using GoRouter together with GoRouterBuilder, as stated in this <a target="_blank" href="https://github.com/flutter/flutter/issues/148185#issuecomment-2107491804">comment</a>.</p>
<h2 id="heading-so-whats-the-problem">So, what’s the problem?</h2>
<p>In our case, the reason why <code>popUntil</code> was causing the infinite loop was that we were using GoRouterBuilder to generate routes, and the generated code calls a static method in <a target="_blank" href="https://github.com/flutter/packages/blob/e4fd6c0e9f1abf4579b916f667996d66bcc7ce89/packages/go_router/lib/src/route_data.dart#L86"><code>route_data.dart</code></a> to create <code>GoRoute</code> objects with a non-null <code>onExit</code> callback.</p>
<pre><code class="lang-dart">  <span class="hljs-comment">/// <span class="markdown">A helper function used by generated code.</span></span>
  <span class="hljs-comment">///
  <span class="markdown">/// Should not be used directly.</span></span>
  <span class="hljs-keyword">static</span> GoRoute $route&lt;T <span class="hljs-keyword">extends</span> GoRouteData&gt;({
    <span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> path,
    <span class="hljs-built_in">String?</span> name,
    <span class="hljs-keyword">required</span> T <span class="hljs-built_in">Function</span>(GoRouterState) <span class="hljs-keyword">factory</span>,
    GlobalKey&lt;NavigatorState&gt;? parentNavigatorKey,
    <span class="hljs-built_in">List</span>&lt;RouteBase&gt; routes = <span class="hljs-keyword">const</span> &lt;RouteBase&gt;[],
  }) {
     ...

    FutureOr&lt;<span class="hljs-built_in">bool</span>&gt; onExit(BuildContext context, GoRouterState state) =&gt;
        factoryImpl(state).onExit(context, state);

    <span class="hljs-keyword">return</span> GoRoute(
      path: path,
      name: name,
      builder: builder,
      pageBuilder: pageBuilder,
      redirect: redirect,
      routes: routes,
      parentNavigatorKey: parentNavigatorKey,
      onExit: onExit, <span class="hljs-comment">// THIS LINE WAS ADDED </span>
    );
  }
</code></pre>
<p>This prevents <a target="_blank" href="https://github.com/flutter/packages/blob/e4fd6c0e9f1abf4579b916f667996d66bcc7ce89/packages/go_router/lib/src/delegate.dart#L175"><code>_completeRouteMatch</code></a> from being called immediately in <a target="_blank" href="https://github.com/flutter/packages/blob/e4fd6c0e9f1abf4579b916f667996d66bcc7ce89/packages/go_router/lib/src/delegate.dart#L138"><code>_handlePopPageWithRouteMatch</code></a>. The next call to <code>pop</code> in the <a target="_blank" href="https://api.flutter.dev/flutter/widgets/NavigatorState/popUntil.html">NavigatorState.popUntil</a> implementation occurs before the scheduled microtask runs, when the route is not popped yet, causing an infinite loop.</p>
<pre><code class="lang-dart">  <span class="hljs-built_in">bool</span> _handlePopPageWithRouteMatch(
      Route&lt;<span class="hljs-built_in">Object?</span>&gt; route, <span class="hljs-built_in">Object?</span> result, RouteMatchBase match) {
    ...
    <span class="hljs-keyword">final</span> RouteBase routeBase = match.route;
    <span class="hljs-keyword">if</span> (routeBase <span class="hljs-keyword">is</span>! GoRoute || routeBase.onExit == <span class="hljs-keyword">null</span>) { <span class="hljs-comment">// onExit IS NEVER NULL</span>
      route.didPop(result);
      _completeRouteMatch(result, match); <span class="hljs-comment">// THIS WILL NEVER BE CALLED</span>
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
    }
    <span class="hljs-comment">// The _handlePopPageWithRouteMatch is called during draw frame, schedule</span>
    <span class="hljs-comment">// a microtask in case the onExit callback want to launch dialog or other</span>
    <span class="hljs-comment">// navigator operations.</span>
    scheduleMicrotask(() <span class="hljs-keyword">async</span> {
      ...
    });
  }
</code></pre>
<p>The same problem occurs in all the custom implementations of <code>popUntil</code> that rely on a <code>pop</code> call.</p>
<h2 id="heading-and-whats-the-solution">And what’s the solution?</h2>
<p>The simplest way I found to implement <code>popUntil</code> correctly without using any <code>pop</code> invocations is to use <a target="_blank" href="https://pub.dev/documentation/go_router/latest/go_router/RouteMatchList/remove.html">RouteMatchBase.remove</a> to "pop" the routes (except the very first one), and then call <a target="_blank" href="https://pub.dev/documentation/go_router/latest/go_router/GoRouterDelegate/setNewRoutePath.html">GoRouterDelegate.setNewRoutePath</a> to set the new routes list.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">extension</span> ContextExtension <span class="hljs-keyword">on</span> BuildContext {
  <span class="hljs-keyword">void</span> popUntil(<span class="hljs-built_in">bool</span> <span class="hljs-built_in">Function</span>(GoRoute route) predicate) {
    <span class="hljs-keyword">final</span> delegate = GoRouter.of(<span class="hljs-keyword">this</span>).routerDelegate;
    <span class="hljs-keyword">var</span> config = delegate.currentConfiguration;
    <span class="hljs-keyword">var</span> routes = config.routes.whereType&lt;GoRoute&gt;();
    <span class="hljs-keyword">while</span> (routes.length &gt; <span class="hljs-number">1</span> &amp;&amp; !predicate(config.last.route)) {
      config = config.remove(config.last);
      routes = config.routes.whereType&lt;GoRoute&gt;();
    }
    delegate.setNewRoutePath(config);
  }
}
</code></pre>
<p>I implemented a full example in this <a target="_blank" href="https://github.com/rubenlop88/go_router_pop_until_example">repo</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>GoRouter was not built specifically to do imperative navigation, but in legacy code that only supports Android and iOS, sometimes it is necessary to use <code>popUntil</code>. For those cases the usual implementations that use <code>pop</code> under the hood do not work after upgrading to GoRouter 14.1.0. Fortunately, we were able to find a solution that works for us. I hope it can work for you too.</p>
]]></content:encoded></item></channel></rss>