Skip to content

[go_router] When replace is called, reuse the same key when possible #2846

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

Merged

Conversation

ValentinVignal
Copy link
Contributor

Should fix flutter/flutter#115902

I've introduced PageKey to be used as a key for the pages instead of ValueKey<String>. In that way, it is easy to know if 2 keys are from the same path or not.

I'm not too confident about my changes in

  • packages/go_router/lib/src/match.dart: It uses the hashCode
  • packages/go_router/lib/src/builder.dart: This is the only place I had to use fullPath. So maybe we could use path directly?

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.

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

Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

I think the push and replace is different enough that we should probably not reuse push in replace method, or have a common private method that just take a non-null page key.

final ValueKey<String> pageKey =
ValueKey<String>('${matches.fullpath}-p$count');
final PageKey pageKey;
if (keyToUse?.path == matches.fullpath) {
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need this check? why not just use keyToUse if it is not null?

Copy link
Contributor Author

@ValentinVignal ValentinVignal Nov 29, 2022

Choose a reason for hiding this comment

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

Because if the page is different and the key doesn't match, ex: /family/:fid and /family, it shouldn't reuse it, right?

I could have named keyToUse keyToReuseIfPossible to be more explicit but it was a bit long

@ValentinVignal
Copy link
Contributor Author

@chunhtai I refactored push and replace to use 2 common private methods in ♻️ Make replace and push use common methods

push(matches); // [push] will notify the listeners.
assert(matches.last.route is! ShellRoute);
PageKey pageKey = _matchList.pop().pageKey;
if (pageKey.path != matches.fullpath) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems weird to sometimes reuse and sometimes push. I am not 100% sure this is the route we should go. I was looking at the navigator API, it looks like this mixes both Navigator.replace and Navigator.pushReplacement. Based on the implemented API, this is more close to pushReplacement. In that case we should probably always trigger a push, and create another API to do the replace

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you suggesting that we implement a

  • pushReplacement that always uses a new key
  • replace that always reuses the key of the replaced page

?

If yes, how would you want me to name those methods? pushReplacement and replace too? I don't know whether it will be considered as a breaking change or not

And in the case of /a being replaced by /b with replace, would it make sense to reuse /as key and see /b's screen while using /a as a key?

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 we should probably rename this to pushReplacement and always does a push. I think implementing replace is a bit risky because we need to worry about people replacing shellRoute. I would probably leave it out for now until we figure out these corner cases

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'm okay to rename the current replace into pushReplacement.
But how do I handle that? Do I make it a breaking change? Or do I deprecate replace and make it return pushReplacement ?

Also, I guess you want this pushReplacement to already generate and use a brand new key, right?
The fix needed to fix flutter/flutter#115902 is to reuse the key when possible

Copy link
Contributor

Choose a reason for hiding this comment

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

I will leave the original issue as a feature request, but I don't think that is something we should do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean you want to leave replace (and reuse the same key) as a feature request but it would not be implemented?

How would suggest I can fix the bug mentioned in flutter/flutter#115902?

Copy link
Contributor

Choose a reason for hiding this comment

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

I can see a lot of corner cases if we implement this API, if you have an idea on how to implement it, feel free to send a pr

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 about something similar to what I implemented in this PR?
It would reuse the same key if the path (ex: /families/:fid) is the same. Otherwise, it creates a new key?

I can modify this PR once #2848 is merged

@MarufHassan17
Copy link

Please add the option which displays depreciated with the new term in VSCode and Android Studio when hovering over replaceNamed

@ValentinVignal ValentinVignal force-pushed the go-router/reuse-same-key-when-replace branch from 9b5769c to f85f785 Compare December 23, 2022 20:58
@ValentinVignal
Copy link
Contributor Author

ValentinVignal commented Dec 23, 2022

@chunhtai What do you think about this implementation?
I didn't implement replaceNamed because I read you wanted to remove the named route API at some point (flutter/flutter#107729)

@ValentinVignal
Copy link
Contributor Author

It looks like the checks are failing because of go_router_builder
I believe #2977 should fix it

@chunhtai
Copy link
Contributor

chunhtai commented Jan 4, 2023

@chunhtai What do you think about this implementation? I didn't implement replaceNamed because I read you wanted to remove the named route API at some point (flutter/flutter#107729)

We decide to keep named location since we couldn't come up with a better alternative, and functionality wise it works fine.

@ValentinVignal
Copy link
Contributor Author

@chunhtai What do you think about this implementation? I didn't implement replaceNamed because I read you wanted to remove the named route API at some point (flutter/flutter#107729)

We decide to keep named location since we couldn't come up with a better alternative, and functionality wise it works fine.

Ok I can add replaceNamed, but before I do that. Is the implementation fine with you?

# Conflicts:
#	packages/go_router/CHANGELOG.md
#	packages/go_router/lib/src/delegate.dart
#	packages/go_router/lib/src/match.dart
#	packages/go_router/lib/src/matching.dart
#	packages/go_router/pubspec.yaml
@ValentinVignal
Copy link
Contributor Author

I implemented replaceNamed in ✨ Add replaceNamed

@ValentinVignal
Copy link
Contributor Author

ValentinVignal commented Jan 9, 2023

Please add the option which displays depreciated with the new term in VSCode and Android Studio when hovering over replaceNamed

@MarufHassan17, I'm not sure I understand what you mean? Did you mean I should add the @Deprecated decorator to replaceNamed? If yes, replaceNamed has already been deleted in #2848 and got renamed into pushReplacementNamed (it is a breaking change)

@ValentinVignal
Copy link
Contributor Author

@chunhtai What do you think of this implementation?

Or do you think the key should always be reused when the user calls replace ?
It's either we

  • Only reuse the same key when the location is the same. But this prevents the user to be able from reusing/preserving any state between 2 similar routes (ex : /my-form/:form-id and /my-form/:form-id/edit) with 2 different locations. And it will force the user to reuse the same route (ex: /my-form/:form-id and /my-form/:form-id?edit=true). I don't know whether it is good or bad.
  • Always reuse the same key. We let the user decide whether or not he wants to keep the state of its widget tree by using replace or lose it by using pushReplacement. This will allow to have animations/reuse the state between 2 similar routes with a different location (ex : /my-form/:form-id and /my-form/:form-id/edit) (and I don't know whether it is good or bad either). However, using the location of the routes for the keys wouldn't make so much sense anymore as the new route could have nothing related to the location of the reused key. And it will need to be changed.

What do you think?

@ValentinVignal ValentinVignal mentioned this pull request Jan 18, 2023
11 tasks
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

sorry for the late reply, I just come back from vacation.

_matchList.remove(_matchList.last);
push(matches); // [push] will notify the listeners.
}

/// Replaces the top-most page of the page stack with the given one and reuse
Copy link
Contributor

Choose a reason for hiding this comment

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

This doc should not talk about page key since that is implementation detail. Instead it should talk about the visual effect of calling this method. Same thing for pushReplacement

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems not addressed yet?

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 I could write in the doc that pushReplacement animates to the new page while replace does not.
But it is not only that, pushReplacement doesn't preserve the state of the widget tree while replace does. I think this is an important aspect that needs to be said and that's why I'm mentioning the page key.

Having said this, what do you think I should write?

Copy link
Contributor

@chunhtai chunhtai Feb 17, 2023

Choose a reason for hiding this comment

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

You should definitely mention animation/page transition and that the it will be treated as same page and state will be preserved. I am ok if either you keep the page key info here or not.

Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't seem to be addressed yet

/// See also:
/// * [push] which pushes the given location onto the page stack.
/// * [pushReplacement] which replaces the top-most page of the page stack but
/// always use a new page key.
Copy link
Contributor

Choose a reason for hiding this comment

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

same here

/// * [push] which pushes the given location onto the page stack.
/// * [pushReplacement] which replaces the top-most page of the page stack but
/// always use a new page key.
void replace(RouteMatchList matches) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This method starts to deviate from Navigator.replace, which can replace any route instead of just the top one. One thing I think that may be useful would be replace GoRoute with an other GoRoute. maybe we can use GoRoute.name as here? WDYT?

also cc @johnpryan for API feedback

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'm not trying to re-implement Navigator.replace API here. The main goal of this PR is to fix flutter/flutter#115902. The solution I came up with was this replace method (which we could name differently, ex: replaceLast). I don't know whether re-implementing Navigator.replace would fix the issue (maybe it will not re-use the same key, like pushReplacement) or not.

Having this in mind, what do you think we should do in this PR?

Copy link
Contributor

@chunhtai chunhtai Feb 2, 2023

Choose a reason for hiding this comment

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

I think the key only matter when replacing the top-most page?

My worry is that someone later on may file another feature request to replace the middle of match list, and then we would need to come up with a new api since the function signature here cannot be reused.

I also been thinking about opening up the RouteMatchList and all the package level private class. That may open a better way to manage the RouteMatchList.

So I think there are two ways to go from here. Either we expand this to be able to replace any GoRoute(or a sub list of RouteBases) with one or more RouteBases. Or we open up the API now and let customers to do their things.

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 I think there are two ways to go from here. Either we expand this to be able to replace any GoRoute(or a sub list of RouteBases) with one or more RouteBases. Or we open up the API now and let customers to do their things.

Is there a way you prefer over the other? I guess I can try to implement the replace method here

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 tried to look a bit at how it could be possible to make replace a bit like

void Navigator.replace({ required Route<dynamic> oldRoute, required Route<T> newRoute})

So I guess, from what you said:

One thing I think that may be useful would be replace GoRoute with an other GoRoute. maybe we can use GoRoute.name as here

replace could have one of the signatures (if routeNameToReplace is null, it would replace the top most route).

void replace(RouteMatchList matchese, {String? routeNameToReplace});
void replace(RouteBase newRoute, {String? routeNameToReplace});

There are several issues with this:

  • If a route is present multiple times in the history, which one is supposed to be replaced?
  • This solution doesn't work with go_router_builder as all the generated routes are nameless.

About your concerns

My worry is that someone later on may file another feature request to replace the middle of match list, and then we would need to come up with a new api since the function signature here cannot be reused.

The signature of the method replace of the delegate could be changed easily as it is not opened yet, so there should not be any issues there.

The signature of the method replace of the router should not be breaking. It could change from

void replace(String location, {Object? extra});

to

void replace(String location, {WhateverType? whateverParameter, Object? extra});

What do you think?

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 still slightly concern about using a string location to do the replace, but i can't find a better alternative. yeah, I am ok with this approach

Copy link
Contributor

Choose a reason for hiding this comment

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

please re-request me for review when this pr is ready.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, I am ok with this approach

What approach are you okay with? The one currently implemented in this PR ? (If yes, it would be ready to review)

If you are talking about

void replace(RouteMatchList matchese, {String? routeNameToReplace});
void replace(RouteBase newRoute, {String? routeNameToReplace});

What about

There are several issues with this:

  • If a route is present multiple times in the history, which one is supposed to be replaced?
  • This solution doesn't work with go_router_builder as all the generated routes are nameless.

?

Copy link
Contributor

Choose a reason for hiding this comment

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

the current implementation

…e-key-when-replace

# Conflicts:
#	packages/go_router/CHANGELOG.md
#	packages/go_router/pubspec.yaml
@chunhtai chunhtai self-requested a review February 8, 2023 18:01
…e-key-when-replace

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

@chunhtai When you review, could you take a look at #2846 (comment)

Or do you think the key should always be reused when the user calls replace ? It's either we

  • Only reuse the same key when the location is the same. But this prevents the user to be able from reusing/preserving any state between 2 similar routes (ex : /my-form/:form-id and /my-form/:form-id/edit) with 2 different locations. And it will force the user to reuse the same route (ex: /my-form/:form-id and /my-form/:form-id?edit=true). I don't know whether it is good or bad.
  • Always reuse the same key. We let the user decide whether or not he wants to keep the state of its widget tree by using replace or lose it by using pushReplacement. This will allow to have animations/reuse the state between 2 similar routes with a different location (ex : /my-form/:form-id and /my-form/:form-id/edit) (and I don't know whether it is good or bad either). However, using the location of the routes for the keys wouldn't make so much sense anymore as the new route could have nothing related to the location of the reused key. And it will need to be changed.

What do you think?

I'm not sure which implementation would be best

@ValentinVignal
Copy link
Contributor Author

@chunhtai I updated the doc in docs: Update the doc of replace to mention animation and state

Tell me if it is good enough for you

@@ -89,4 +99,29 @@ extension GoRouterHelper on BuildContext {
queryParams: queryParams,
extra: extra,
);

/// Replaces the top-most page of the page stack with the given one.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you also mention the transition and preserve state behavior somewhere in here as well?

@@ -272,6 +282,45 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
);
}

/// Replaces the top-most page of the page stack with the given one.
///
Copy link
Contributor

Choose a reason for hiding this comment

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

here as well


/// Replaces the top-most page of the page stack with the named route w/
/// optional parameters, e.g. `name='person', params={'fid': 'f2', 'pid':
/// 'p1'}`.
Copy link
Contributor

Choose a reason for hiding this comment

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

here as well

@ValentinVignal
Copy link
Contributor Author

@chunhtai Sorry for the misses, I added the doc in docs: Add more doc to replace and replaceNamed

@johnpryan
Copy link
Contributor

@stuartmorgan @ditman can you help us figure out why CI is failing?

@ditman
Copy link
Member

ditman commented Feb 22, 2023

@stuartmorgan @ditman can you help us figure out why CI is failing?

@johnpryan "submit-queue" is the check that asserts that whatever is at the tip of flutter/packages main branch is passing all post-submit tests.

It looks red now, because flutter/packages main is undergoing testing (here), but once that finishes correctly, it'll turn green in this PR as well.

ValentinVignal and others added 2 commits February 23, 2023 09:21
…e-key-when-replace

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

ditman commented Mar 1, 2023

Updated branch to fix merge conflicts.

@chunhtai I've seen you've made some comments after LGTMing. Please, take another look, and if you're still good with this landing, set the autosubmit label (else withdraw your approval, please!) :P

@chunhtai chunhtai self-requested a review March 2, 2023 22:42
…e-key-when-replace

# Conflicts:
#	packages/go_router/CHANGELOG.md
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

some of the doc is still missing

Comment on lines 163 to 164
/// Replaces the top-most page of the page stack with the given one. The page
/// key of the new page will always be different from the old one.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Replaces the top-most page of the page stack with the given one. The page
/// key of the new page will always be different from the old one.
/// Replaces the top-most page of the page stack with the given one.
///
/// The page key of the new page will always be different from the old one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry for that. Hopefully docs: Improve the documentation addressed all your comments

_matchList.remove(_matchList.last);
push(matches); // [push] will notify the listeners.
}

/// Replaces the top-most page of the page stack with the given one and reuse
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't seem to be addressed yet

@@ -272,6 +282,48 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
);
}

/// Replaces the top-most page of the page stack with the given one but treats
/// it as the same page. The page key will be reused. This will preserve the
Copy link
Contributor

Choose a reason for hiding this comment

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

The first paragraph should always be one brief sentence

});
}

/// Replaces the top-most page of the page stack with the named route w/
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Replaces the top-most page of the page stack with the named route w/
/// Replaces the top-most page of the page stack with the named route with


/// Replaces the top-most page of the page stack with the named route w/
/// optional parameters, e.g. `name='person', params={'fid': 'f2', 'pid':
/// 'p1'}`. But it treats it as the same page. The page key will be reused.
Copy link
Contributor

Choose a reason for hiding this comment

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

The first paragraph should be one sentence

@@ -89,4 +99,32 @@ extension GoRouterHelper on BuildContext {
queryParams: queryParams,
extra: extra,
);

/// Replaces the top-most page of the page stack with the given one but treats
/// it as the same page. The page key will be reused. This will preserve the
Copy link
Contributor

Choose a reason for hiding this comment

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

same


/// Replaces the top-most page of the page stack with the named route w/
/// optional parameters, e.g. `name='person', params={'fid': 'f2', 'pid':
/// 'p1'}`. But it treats it as the same page. The page key will be reused.
Copy link
Contributor

Choose a reason for hiding this comment

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

same

@ValentinVignal
Copy link
Contributor Author

ValentinVignal commented Mar 10, 2023

@chunhtai @Hangyujin is there anything else I need to do ?

@ValentinVignal
Copy link
Contributor Author

@chunhtai @Hangyujin If the code looks good to you, do you think we can go through with this PR?

Copy link
Member

@hannah-hyj hannah-hyj left a comment

Choose a reason for hiding this comment

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

LGTM

@chunhtai chunhtai added the autosubmit Merge PR when tree becomes green via auto submit App label Mar 16, 2023
@ValentinVignal
Copy link
Contributor Author

@chunhtai @Hangyujin , the check submit-queue keeps failing on this PR (and not on #3462 for example), am I supposed to do something about it?

@chunhtai
Copy link
Contributor

@ValentinVignal no that means the current tip of tree is broken, it would be us to fix it. No action item on your side.

@auto-submit
Copy link
Contributor

auto-submit bot commented Mar 21, 2023

auto label is removed for flutter/packages, pr: 2846, due to - The status or check suite Windows win32-platform_tests stable - packages has failed. Please fix the issues identified (or deflake) before re-applying this label.

@auto-submit auto-submit bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Mar 21, 2023
@chunhtai chunhtai added the autosubmit Merge PR when tree becomes green via auto submit App label Mar 21, 2023
@auto-submit auto-submit bot merged commit bb3bfd6 into flutter:main Mar 21, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Mar 22, 2023
nploi pushed a commit to nploi/packages that referenced this pull request Jul 16, 2023
flutter#2846)

[go_router] When `replace` is called, reuse the same key when possible
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
autosubmit Merge PR when tree becomes green via auto submit App p: go_router
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[go_router] replace should reuse the same key as the popped page when possible
6 participants