Alternative to popUntil in go router in flutter
Asked Answered
C

6

11

I was wonder if there is alternative to Navigator's popUntil() method, or any workaround to achieve the same.

If i were to use Navigator. I would use like so:

void _logout() {
  Navigator.popUntil(context, ModalRoute.withName('/login'));
}

How to achieve the same in go router ?


I see a github issue - [go_router] Implement popUntil #2728, but there is no positive outcome of the same.

Curiosity answered 29/3, 2023 at 5:19 Comment(3)
Do you mean you want to go to the login route, remove all routes below it and keep routes above it (if any)?Dyadic
@PeterKoltai, I mean if you see navigation as a stack I want to pop everything till login and anything below it remains as isCuriosity
Simply using context.goNamed('/login') does not work in this case? If there are routes which are not included in /login named route, they will be removed from the stack.Dyadic
I
14

WORKAROUND


While the answer from @CommonSense works, maybe we could tweak it a little, avoiding pop when GoRouter cannot pop.

And have this in mind. At the end, this is just a workaround and can leave to unexpected behaviors, specially with GoRouter upgrades in the future.

void popUntilPath(String routePath) {
    
  final router = GoRouter.of(context);
  while (router.location != routePath) {

    if (!router.canPop()) {
      return;
    }

    debugPrint('Popping ${router.location}');
    router.pop();
  }
}

THE RIGHT WAY?


Another way to do that (which seems to be the right way of doing this) is to use .go with the correct route declaration, like this:

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Go Router Demo',
      routerConfig: GoRouter(
        initialLocation: '/login',
        routes: [
          GoRoute(
            name: '/login',
            path: '/login',
            builder: (_, __) => const Login(),
            routes: [
              GoRoute(
                name: 'step-1',
                path: 'step-1',
                builder: (_, __) => const Step1(),
              ),
              GoRoute(
                name: 'step-2',
                path: 'step-2',
                builder: (_, __) => const Step2(),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class Login extends StatelessWidget {
  const Login({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Entry Page')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Login entry page'),
            ElevatedButton(
              onPressed: () => GoRouter.of(context).pushNamed('step-1'),
              child: const Text('Go to step 1'),
            ),
          ],
        ),
      ),
    );
  }
}

class Step1 extends StatelessWidget {
  const Step1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Step 1')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Step 1'),
            ElevatedButton(
              onPressed: () => GoRouter.of(context).pushNamed('step-2'),
              child: const Text('Go to step 2'),
            ),
          ],
        ),
      ),
    );
  }
}

class Step2 extends StatelessWidget {
  const Step2({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Step 2')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Step 2'),
            ElevatedButton(
              onPressed: () => GoRouter.of(context).goNamed('/login'),
              child: const Text('Go to login'),
            ),
          ],
        ),
      ),
    );
  }
}


When you call goNamed to /login, it will do a pop until login, which is what you want.

If you don't declare you routes like this (maybe you are using the imperative API) the GoRouter will go to that route but by adding it into the stack and not just removing the other routes.

Implosive answered 1/6, 2023 at 12:54 Comment(2)
I can confirm: I tried "the right way" and it works exactly as expected! Thanks!Eiten
That being said, your workaround can be more convenient in complex navigation.Eiten
H
6

go_router: 10.2.0

///Navigate back to specific route
void popUntilPath(BuildContext context, String routePath) {
 while (
  router.routerDelegate.currentConfiguration.matches.last.matchedLocation !=
      routePath) {
  if (!context.canPop()) {
  return;
  }
 context.pop();
 }
}
Hanleigh answered 21/9, 2023 at 8:19 Comment(0)
D
5

Hope this helps

while (GoRouter.of(context).location != "/") {
    debugPrint(GoRouter.of(context).location.toString()); //to get the routes in between
    GoRouter.of(context).pop();
}
Dapplegray answered 25/5, 2023 at 6:5 Comment(5)
it works, but sometimes it seems that you can see the pages being poppedGenitalia
Acacio You can use material or other transition for that.Dapplegray
yes, but then you would also need to change the transition for push, right?Genitalia
Not necessarily. What you are looking for might have been a bug in older version of GoRouter but it's fixed in this pull request. If you still feel there is some pop transition issue, you can open a fix request issue in it and we will look into it.Dapplegray
@CommonSense This doesn't work anymore in GoRouter v.10Hardee
P
4

I am using the following extension to achieve this.

extension GoRouterExtension on GoRouter {
  // Navigate back to a specific route
  void popUntilPath(String ancestorPath) {
    while (routerDelegate.currentConfiguration.matches.last.matchedLocation !=
        ancestorPath) {
      if (!canPop()) {
        return;
      }
      pop();
    }
  }
}

The caller will be something like this:

GoRouter.of(context).popUntilPath(desiredAncestorPath);
Pissarro answered 14/10, 2023 at 4:56 Comment(0)
F
1

Tested this solution with go_router: ^9.0.1

Create a class and extend NavigatorObserver.

class MyNavigatorObserver extends NavigatorObserver {
  static List<String> backStack = [];

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    backStack.add(route.settings.name ?? '');
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    backStack.removeLast();
  }
}

Pass the observer while initiating GoRouter

GoRouter(
  initialLocation: initialLocation,
  observers: [ MyNavigatorObserver() ],
  ...
  ...
)

Use this function to pop until your desired route

void popUntil(String routeName) {
  if (!MyNavigatorObserver.backStack.contains(routeName))
      return;

  while (MyNavigatorObserver.backStack.last != RouteNames.bottomNav) {
    GoRouter.of(context).pop();
  }
}
Frontality answered 23/8, 2023 at 20:59 Comment(0)
P
0

You can use this extension method

extension GoRouterExt on GoRouter {
  String get _currentRoute => routerDelegate.currentConfiguration.matches.last.matchedLocation;

  /// Pop until the route with the given [path] is reached.
  /// Example
  /// ``` dart
  ///  GoRouter.of(context).popUntil(SettingsScreen.route);
  /// ```

  void popUntil(String path) {
    var currentRoute = _currentRoute;
    while (currentRoute != path && canPop()) {
      YourLogger.log('currentRoute: $currentRoute');
      pop();
      currentRoute = _currentRoute;
    }
  }
}
Plumbiferous answered 19/7 at 21:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.