Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[go_router] Implement popUntil #2728

Closed
wants to merge 11 commits into from

Conversation

ValentinVignal
Copy link
Contributor

@ValentinVignal ValentinVignal commented Oct 22, 2022

So I wanted to work on flutter/flutter#107052 for pushAndRemoveUntil but it was closed saying it was a duplicate of flutter/flutter#106402 which was not because it didn't contain the request for pushAndRemoveUntil. And flutter/flutter#106402 got also closed because it got implemented.

So do you want me to re-create an issue? Or should we re-open flutter/flutter#107052?


In any case, this PR first implements popUntil which is simpler than pushAndRemoveUntil. I will implement pushAndRemoveUntil if the implementation of this one is acceptable.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the relevant style guides and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use dart format.)
  • I signed the CLA.
  • The title of the PR starts with the name of the package surrounded by square brackets, e.g. [shared_preferences]
  • I listed at least one issue that this PR fixes in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.
  • I updated CHANGELOG.md to add a description of the change, following repository CHANGELOG style.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

@ValentinVignal
Copy link
Contributor Author

Also relates to flutter/flutter#99112

@ValentinVignal
Copy link
Contributor Author

Is there something else I need to do in this PR?

@chunhtai
Copy link
Contributor

chunhtai commented Nov 2, 2022

I think these issues are closed due to we plan to use naive navigator API to handle imperative API. @johnpryan can chime in.

@@ -146,6 +146,18 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
notifyListeners();
}

/// Calls [pop] repeatedly until the predicate returns true.
void popUntil(bool Function(RouteMatch) predicate) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just use go to build up to a certain routematch list? If we do refactor the imperative API this will be obsolete

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean?
Are you suggesting that instead of calling popUntil (as a dev using go_router), I should call go?

I don't think it would do the same right?

If I understood correctly, go is kind of a replaceAll, after using go, there is no page to pop and canPop is false.
While popUntil will allow you to only pop some pages but not all. The use case is when the user finishes a wizard to set up something, you want to remove all the wizard's pages from the history but still keep the history the user accumulated before starting the wizard.

go wouldn't work in this scenario, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no I meant it seems like the go can replace popUntil. Since developer creates their own route table, they will be able to use go to achieve functional equivalence of popUntil. For example if they have route table like this

GoRouteA(
  routes: [
    GoRouteB(
      routes: [GoRouteC()]
    )
  ],
)

lets say route match list is [GoRouteA, GoRouteB, GoRouteC].
popUntil((route) => route == GoRouteB) will be the same as go(GoRouteB)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comes back to flutter/flutter#99112 's discussion.

Here you are assuming that the match list is [GoRouteA, GoRouteB, GoRouteC] and matches perfectly the structure

GoRouteA(
  routes: [
    GoRouteB(
      routes: [GoRouteC()]
    )
  ],
)

And pop from GoRouteC always goes "back" to GoRouteB.

But as I described in flutter/flutter#99112 (comment), this is not what users are usually expecting. They don't always access GoRouteC from GoRouteB. So always "going back" to GoRouteB from GoRouteC doesn't work.
The way I understand it is that you are trying to merge/combine "app location" and "history"? But that's 2 different things that should be independent.

Am I missing something?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, you are right, the previous comment is under the assuming that the match list matches the structure. I am assuming history means the internal route stack, not the browser history. In that case, my proposal user can still modify the history through imperative API, it just that they won't be able to change app location with imperative API. Let's continue the discussion in the issue.

# Conflicts:
#	packages/go_router/CHANGELOG.md
#	packages/go_router/pubspec.yaml
@ValentinVignal
Copy link
Contributor Author

Hello @chunhtai , @johnpryan , can I ask for a follow up on this?

Should I close this PR? If yes, is there a plan to make the delegate and route match list public? And can I help on that?

/// Calls [pop] repeatedly until the predicate returns true.
void popUntil(bool Function(RouteMatch) predicate) {
bool hasPopped = false;
while (!predicate(_matchList.last)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

things has changed a bit, its probably the best to call pop directly since it will remove any pageless route that are on top such as dialog.

Copy link

@AceChen1 AceChen1 Dec 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chunhtai It's hard to use go router.... Us project previous use more ‘pop with value on pages’, now have the requirement need do the web compatible, but go router no this feature to provide, then use 'Go_Router_Flow' lib, but looks like Pop function have been changed....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I mean the implementation of popUntil should use pop method. I am on board with implementing popUntil

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 3);
goRouter.routerDelegate.addListener(expectAsync0(count: 2, () {}));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current implementation (simply calling pop, the notifiers are notified for each pop). In this case, 2 times. Is it correct or should the router delegate notify only once?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we may need to refactor the code a bit, every time it notify the listener, the Gorouter will also notify its listener about location change, so if url = '/a/b/c/d/e' and popUtil to '/a'
if we notify each time it pop
goRoute will notify its listener with
'/a/b/c/d'
'/a/b/c'
'/a/b'
'/a'

instead just
'/a'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, to be correct, this test should pass with count: 1, is that correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @chunhtai , I'm not sure it will be possible to notify the listeners only once even when popping several times (and make the test pass with count: 1).

I pushed some code in 2c48f39 . Itis not correct as I guess it doesn't work with multiple nested navigators, it's more of a demonstration example. it uses directly popUntil from the NavigatorState and it still notifies the listeners twice.

Or would you know a way to make this work?

# Conflicts:
#	packages/go_router/CHANGELOG.md
#	packages/go_router/pubspec.yaml
@followthemoney1
Copy link

Will this work for dialogs? I mean if I've open the dialog and then will call this popUntill, will predicate return the right value?

@stuartmorgan
Copy link
Contributor

@chunhtai What's the status of this PR? It looks like it's probably waiting for another review.

@ValentinVignal
Copy link
Contributor Author

@stuartmorgan Actually, it requires some work from me first. #2728 (comment) I need to find a way to make the delegate notify its listeners only once.

I was waiting for #2846 to be merged. Because I personally have a bigger need for replace than popUntil in my projects so I was focusing my energy there

@stuartmorgan
Copy link
Contributor

Thanks, I'm going to go ahead and mark this as a draft then just to get it off of our review queue. Please set it back to Ready for Review once you've had a chance to update it!

@stuartmorgan stuartmorgan marked this pull request as draft January 31, 2023 20:44
# Conflicts:
#	packages/go_router/CHANGELOG.md
#	packages/go_router/lib/src/misc/extensions.dart
#	packages/go_router/pubspec.yaml
@da-kami da-kami mentioned this pull request Mar 1, 2023
5 tasks
@chunhtai
Copy link
Contributor

@ValentinVignal The #2846 is about to be merged (just waiting for ci), will you work on this PR next?

@ValentinVignal
Copy link
Contributor Author

@ValentinVignal The #2846 is about to be merged (just waiting for ci), will you work on this PR next?

I'm not sure I'll work on it right away as I'm quite busy right now, but I'll work on it at some point when #2846 is merged yes. But feel free to take it over if you want to

@chunhtai
Copy link
Contributor

Let me close this one for now, feel free to reopen once you have time.

@talamaska
Copy link

talamaska commented Apr 12, 2023

I'm still missing popUntil. It is irreplace-able feature that I'm missing. go_router should have it if it wants to be a full featured navigator. Some guy told me I could just use .go instead, but isn't .go like a push, won't it create another item in the navigation stack? I don't want that. I want to pop several pages. Use case is very simple. Page with a button, when the button is clicked navigates to a page with camera view, take photo, navigate to a full page preview pf the photo, click on confirm button, pop the preview page and the camera view. The only way to make this now is with 2 pops and have a special logic to get a value from the first pop. What if I have a wizard form with multiple pages. Having to implement a bunch of custom logic and calls of unknown number of pop.

@johnpryan
Copy link
Contributor

@talamaska go() is different from push(), go() replaces your current Navigator stack with a new one. For example, take this sample with two routes:

GoRoute(
  path: '/library',
  routes: [
    GoRoute(
      path: 'album/:albumId',
      routes: [
        GoRoute(
          path: 'song/:songId',
        ),
      ],
    ),
  ],
),

If you navigate to/library/album/1 by tapping on an item, the Navigator will have two Pages, one for /library and one for album/:albumId. If you tap the 'Library' button in the bottom navigation bar, the app will call go('/library') and the Navigator is rebuilt with only one Page, /library, so the second screen is popped from the Navigator and the correct transition animation is displayed, as if the user pressed the in-app back button.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants