[go: nahoru, domu]

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] Add popUntil #6306

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from

Conversation

ValentinVignal
Copy link
Contributor
@ValentinVignal ValentinVignal commented Mar 12, 2024

Part of flutter/flutter#144924

Closes flutter/flutter#131625

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Comment on lines 154 to 163
int count = 0; // Only pop 1 page at the time.
state?.popUntil((Route<dynamic> route) {
if (count == 1 || predicate(walker, route)) {
return true;
}
count++;
return false;
});
return state != null;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here I'm only calling predicate once.
The reason is that after poping, the walker might need to be re-computed in case the popped route was a route from go router.

Then why use popUntil and not pop if I pop only once? It is to be able to provide the Route<dynamic> route parameter to the predicate. I cannot access the latest route of the NavigatorState since NavigatorState._history and NavigatorState._lastRouteEntryWhereOrNull are private.


Do you have a better idea or suggestions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@chunhtai Do you have any feedback on this?

Copy link
Contributor

Choose a reason for hiding this comment

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

is there a use case for Route<dynamic> route? it seems a bit out of place that we all deal with RouteBase and GoRouterState in this package.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The case I'm thinking about and I've been facing in some applications is when there are dialogs / bottom sheets / popup menus (or any other imperative routes) open

I also worked on some projects where the code base uses both go_router and Navigator.of(context).push. Either because they didn't get the time to migrate all the pages yet or because they don't want those pages to be linked to a specific URL on the web

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, my suggestion would be changing routematch to gorouterstate.

@ValentinVignal
Copy link
Contributor Author

@chunhtai @johnpryan @hangyujin , do you have any feedback on this PR?

/// Signature for a predicate function that determines whether a route should be
/// popped or not.
typedef PopUntilPredicate = bool Function(
RouteMatchBase,
Copy link
Contributor

Choose a reason for hiding this comment

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

RouteMatchBase is currently used internal only, we usually use GoRouterState for public facing API

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 154 to 163
int count = 0; // Only pop 1 page at the time.
state?.popUntil((Route<dynamic> route) {
if (count == 1 || predicate(walker, route)) {
return true;
}
count++;
return false;
});
return state != null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a use case for Route<dynamic> route? it seems a bit out of place that we all deal with RouteBase and GoRouterState in this package.

/// Pop the Navigator's page stack until the predicate returns `true`.
void popUntil(PopUntilPredicate predicate) {
bool hasStoppedPopping = false;
bool popUntilPredicate(GoRouterState state, Route<dynamic> route) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not entirely sure why this method is needed. it seems to me that you can use predicate directly instead of wrapping in a method that doesn't do much

}

while (!hasStoppedPopping) {
final bool couldBePopped = _popWithPredicate(popUntilPredicate);
Copy link
Contributor

Choose a reason for hiding this comment

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

why not just let _popWithPredicate to return true if the predicate after pop return false? an then you can basically do

while(_popWithPredicate(popUntilPredicate));

Copy link
Contributor

Choose a reason for hiding this comment

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

something like this

  void popUntil(PopUntilPredicate predicate) {
    while (_popWithPredicate(predicate)) {}
  }

  bool _popWithPredicate(
    PopUntilPredicate predicate,
  ) {
    NavigatorState? state;

    if (navigatorKey.currentState?.canPop() ?? false) {
      state = navigatorKey.currentState;
    }
    List<RouteMatchBase> matches = currentConfiguration.matches;
    while (matches.last is ShellRouteMatch) {
      final ShellRouteMatch leaf = matches.last as ShellRouteMatch;
      if (leaf.navigatorKey.currentState?.canPop() ?? false) {
        state = leaf.navigatorKey.currentState;
      }
      matches = leaf.matches;
    }
    bool predicateMatched = false;
    state?.popUntil((Route<dynamic> route) {
      final RouteMatchBase leaf = matches.last;
      if (!state!.canPop() || leaf is ShellRouteMatch) {
        // If the leaf is ShellRouteMatch, it needs to pop leaf's navigator
        // first.
        return true;
      }
      predicateMatched = predicate(leaf.buildState(_configuration, currentConfiguration), route,);
      return predicateMatched;
    });
    return state != null && !predicateMatched;
  }

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 think of refactor: Simplify the code by removing a method ?

If that's fine I can go with it and work on adding more tests

Copy link
Contributor

Choose a reason for hiding this comment

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

yes that looks fine

@@ -4770,6 +4770,62 @@ void main() {
expect(find.byKey(heroKey), findsNothing);
});
});

group('popUntil', () {
testWidgets(
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably have more tests on cases where nested shellroute.

@chunhtai
Copy link
Contributor
chunhtai commented May 2, 2024

Hi @ValentinVignal , are you still working on this one?

@ValentinVignal
Copy link
Contributor Author
ValentinVignal commented May 3, 2024

Yes, but I'm trying to finish onExit first, I want to make sure I get the right logic to retrieve the state there before I do it here too

@chunhtai chunhtai marked this pull request as draft May 16, 2024 21:39
@chunhtai
Copy link
Contributor

converting to draft for now, please convert back to pr once it is ready

# Conflicts:
#	packages/go_router/CHANGELOG.md
#	packages/go_router/pubspec.yaml
@chunhtai
Copy link
Contributor
chunhtai commented Jun 6, 2024

Hi @ValentinVignal is this ready for another look?

@ValentinVignal
Copy link
Contributor Author

@chunhtai I left you a message here, I was waiting for your feed back

#6306 (comment)

@ValentinVignal
Copy link
Contributor Author
ValentinVignal commented Jun 10, 2024

@chunhtai I'm facing an issue with ShellRoute that I'm not really sure how to handle.
I wrote some tests in test: Add some tests and the last one ("It should pop the last 2 routes and the ShellRoute with it") is failing.

It successfully pops /a/b/c but when it tries to pop /a/b (and the shell route with it), it ends up with RouteMatchBase._removeRouteMatchFromList:

static List<RouteMatchBase> _removeRouteMatchFromList(
List<RouteMatchBase> matches, RouteMatchBase target) {
// Remove is caused by pop; therefore, start searching from the end.
for (int index = matches.length - 1; index >= 0; index -= 1) {
final RouteMatchBase match = matches[index];
if (match == target) {
// Remove any redirect only route immediately before the target.
while (index > 0) {

with matches being "correct" [RouteMatch('/'), RouteMatch('/a'), ShellRouteMatch].

But target is "outdated",
it is also a ShellRouteMatch, but it still has 2 items in its matches (c didn't get popped) ; unlike the ShellRouteMatch from matches which now has only b in its matches (c correctly got popped).

Because of that

if (match == target) {

is false

and the ShellRouteMatch is never popped.


Looking at the code, the outdated target variable comes from

bool _handlePopPage(Route<Object?> route, Object? result) {
final Page<Object?> page = route.settings as Page<Object?>;
final RouteMatchBase match = _pageToRouteMatchBase[page]!;
return widget.onPopPageWithRouteMatch(route, result, match);
}

The _page variable is only updated in the build method,

@override
Widget build(BuildContext context) {
if (_pages == null) {
_updatePages(context);
}
assert(_pages != null);
return GoRouterStateRegistryScope(

but there is no build run while using popUntil.

This strategy of updating the state in the build method looks suspicious to me, I guess this is to provide a valid context to the different buildPages.

Do you have any suggestions on what we could do here?

@chunhtai chunhtai self-requested a review June 13, 2024 21:47
@ValentinVignal
Copy link
Contributor Author
ValentinVignal commented Jul 1, 2024

@chunhtai Do you have some feedback on this?

# Conflicts:
#	packages/go_router/CHANGELOG.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants