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] Return a value on pop #2416

Closed
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2f515a8
Added Asyncs pushes and a pop result
NazarenoCavazzon Jul 9, 2022
25577e2
Update go_router.dart
NazarenoCavazzon Jul 9, 2022
2be6daa
Added pushAsync
NazarenoCavazzon Jul 9, 2022
05ab251
Added documentation to the _completers variable
NazarenoCavazzon Jul 9, 2022
9f20a71
Update go_router.dart
NazarenoCavazzon Jul 9, 2022
90954e1
Update go_router_delegate.dart
NazarenoCavazzon Jul 9, 2022
1750b55
Changed T to dynamic
NazarenoCavazzon Jul 9, 2022
06a2ae5
Update go_router.dart
NazarenoCavazzon Jul 9, 2022
b77cc10
Revert "Update go_router.dart"
NazarenoCavazzon Jul 9, 2022
a5a1df9
CHANGELOG updated and version updated in the pubspec.yaml
NazarenoCavazzon Jul 9, 2022
977606e
Added a space
NazarenoCavazzon Jul 9, 2022
d57d39f
Merge remote-tracking branch 'upstream/main' into feat/pop-with-result
NazarenoCavazzon Aug 4, 2022
f1bfddd
Test
NazarenoCavazzon Aug 4, 2022
46d5899
Extension update
NazarenoCavazzon Aug 4, 2022
38814a8
Update extensions.dart
NazarenoCavazzon Aug 4, 2022
8e99376
Update extensions.dart
NazarenoCavazzon Aug 4, 2022
1e6abfe
Test working
NazarenoCavazzon Aug 4, 2022
87818e2
Pop with result working
NazarenoCavazzon Aug 4, 2022
6de9f2a
Test Made
NazarenoCavazzon Aug 4, 2022
121c560
Changelog and version updated
NazarenoCavazzon Aug 4, 2022
63057b5
Changed dynamic to T
NazarenoCavazzon Aug 4, 2022
2c56459
Pop updated to T
NazarenoCavazzon Aug 4, 2022
48688b8
Merge branch 'feat/return-value-on-pop' into feat/pop-with-result
NazarenoCavazzon Aug 5, 2022
5b94f01
Updated pop in extensions :)
NazarenoCavazzon Aug 5, 2022
fbc8d0c
Merge branch 'feat/return-value-on-pop' into feat/pop-with-result
NazarenoCavazzon Aug 5, 2022
dd05959
Update readme
NazarenoCavazzon Aug 5, 2022
5e27ae3
Updated comments and also added documentation for this in the readme
NazarenoCavazzon Aug 5, 2022
0488a2e
Changed sync methods to the async ones
NazarenoCavazzon Aug 5, 2022
10e1181
Merge branch 'feat/return-value-on-pop' into feat/pop-with-result
NazarenoCavazzon Aug 5, 2022
b7431c8
Update Replace
NazarenoCavazzon Aug 8, 2022
3fb8ccd
Update on how we handle replace
NazarenoCavazzon Aug 8, 2022
0061c32
element updated to element2
NazarenoCavazzon Aug 8, 2022
5fffdf3
go_router_builder version updated
NazarenoCavazzon Aug 8, 2022
133e621
Merged main
NazarenoCavazzon Aug 8, 2022
12c7d13
Merge branch 'feat/return-value-on-pop' into feat/pop-with-result
NazarenoCavazzon Aug 11, 2022
12eef5e
go and go named async
NazarenoCavazzon Aug 12, 2022
30449ef
Test updated for go and goNamed
NazarenoCavazzon Aug 12, 2022
57f7f82
Merge remote-tracking branch 'upstream/main' into feat/return-value-o…
NazarenoCavazzon Aug 16, 2022
a1397dc
Completers saved in RouteMatchs rather than in a completer list in th…
NazarenoCavazzon Aug 16, 2022
6fe7ae0
Merge remote-tracking branch 'upstream/main' into feat/return-value-o…
NazarenoCavazzon Aug 18, 2022
5a7418d
Updated PR
NazarenoCavazzon Aug 18, 2022
f8eab3a
Merge remote-tracking branch 'upstream/main' into feat/return-value-o…
NazarenoCavazzon Sep 27, 2022
6f9fa08
Final updates
NazarenoCavazzon Sep 27, 2022
5d909fe
Merge remote-tracking branch 'upstream/main' into feat/return-value-o…
NazarenoCavazzon Oct 12, 2022
6ef8dfd
Update CHANGELOG.md
NazarenoCavazzon Oct 12, 2022
ce29ba7
Final updates
NazarenoCavazzon Oct 12, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/go_router/CHANGELOG.md
@@ -1,3 +1,8 @@
## 4.3.0

- Adds a promise option to `push` and `pushNamed` making it posible to await results from a `pop`.
- Adds an optional parameter to the `pop` method, allowing to return values from a `pop`.

## 4.2.7

- Update README
Expand Down
14 changes: 14 additions & 0 deletions packages/go_router/README.md
Expand Up @@ -134,6 +134,20 @@ methods:
onTap: () => context.go('/page2')
```


To wait for values when the screen pops:

```dart
onTap: () {
// In the new page you can do 'context.pop<bool>(someValue)' to return a value.
final bool? result = await context.push<bool>('/page2');

WidgetsBinding.instance.addPostFrameCallback((_) {
if(result ?? false)...
});
}
```

### Still not sure how to proceed?
See [examples](https://github.com/flutter/packages/tree/main/packages/go_router/example) for complete runnable examples or visit [API documentation](https://pub.dev/documentation/go_router/latest/go_router/go_router-library.html)

Expand Down
40 changes: 35 additions & 5 deletions packages/go_router/lib/src/delegate.dart
Expand Up @@ -44,13 +44,21 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>

final GlobalKey<NavigatorState> _key = GlobalKey<NavigatorState>();

/// The list of completers for the promises when pushing asynchronous routes.
Copy link
Contributor

Choose a reason for hiding this comment

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

It would probably make more sense to store the completers on RouteMatch. We would need RouteMatch to implement == and hashCode and make RouteMatchList check for equality rather than treating them as immutable.

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 trying with this approach, the main problem are the go and goNamed, where would I store the completer?

Copy link
Contributor

Choose a reason for hiding this comment

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

go and goNamed should create a RouteMatch - I'm not sure I understand your question though.

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 the problem is that go and goNamed don't create a RouteMatch, but we can create them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnpryan After going into the nature of the go and goNamed methods it doesn't make any sense to implement this into the method, because it basically redirect to a specific path removing all the things below it.

You can read more about it here

I'm pushing changes for your review rn.

final Map<String, Completer<dynamic>> _completers =
<String, Completer<dynamic>>{};
RouteMatchList _matches = RouteMatchList.empty();
final Map<String, int> _pushCounts = <String, int>{};

/// Pushes the given location onto the page stack
void push(RouteMatch match) {
// Remap the pageKey to allow any number of the same page on the stack
/// Pushes the given location onto the page stack with an optional promise.
Future<T?> push<T extends Object?>(RouteMatch match) {
// Remap the pageKey to allow any number of the same page on the stack.
final String fullPath = match.fullpath;

// Create a completer for the promise and store it in the completers map.
final Completer<T> completer = Completer<T>();
_completers[fullPath] = completer;
Copy link

Choose a reason for hiding this comment

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

It's should be replaced by final Completer<T?> completer = Completer<T?>();
If T is a nonnull type, app will crash when users pressing the Back button (complete with a null value).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will not because T extends Object?, so T is out of the box nullable

Copy link

Choose a reason for hiding this comment

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

If I write:
Future<bool?> result = context.push("someRoute")
T will be inferred as bool, not bool?. I can only fix by specifying T:
Future<bool?> result = context.push<bool?>("someRoute")

I tested with flutter master channel.

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 changed it, hope that fixes it


final int count = (_pushCounts[fullPath] ?? 0) + 1;
_pushCounts[fullPath] = count;
final ValueKey<String> pageKey = ValueKey<String>('$fullPath-p$count');
Expand All @@ -67,15 +75,27 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>

_matches.push(newPageKeyMatch);
notifyListeners();
return completer.future;
}

/// Returns `true` if there is more than 1 page on the stack.
bool canPop() {
return _matches.canPop();
}

/// Pop the top page off the GoRouter's page stack.
void pop() {
/// Pop the top page off the GoRouter's page stack and complete a promise if
/// there is one.
void pop<T extends Object?>([T? value]) {
johnpryan marked this conversation as resolved.
Show resolved Hide resolved
final RouteMatch last = _matches.last;

// If there is a promise for this page, complete it.
final Completer<T>? completer = _completers[last.fullpath] as Completer<T>?;
if (completer != null) {
completer.complete(value);
}

// Remove promise from completers map.
_completers.remove(last.fullpath);
Copy link

Choose a reason for hiding this comment

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

I think the completer should be removed from the list as well when users call void replace(RouteMatch match). Because the last RouteMatch was removed.

_matches.pop();
notifyListeners();
}
Expand All @@ -85,7 +105,17 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
/// See also:
/// * [push] which pushes the given location onto the page stack.
void replace(RouteMatch match) {
final String lastPath = _matches.last.fullpath;
final Completer<dynamic>? completer = _completers[lastPath];

// If there's a promise for the last page, we update it to make it point to
// the new page.
if (completer != null) {
_completers[match.fullpath] = completer;
_completers.remove(lastPath);
}
_matches.matches.last = match;

notifyListeners();
}

Expand Down
12 changes: 6 additions & 6 deletions packages/go_router/lib/src/misc/extensions.dart
Expand Up @@ -36,18 +36,18 @@ extension GoRouterHelper on BuildContext {
extra: extra,
);

/// Push a location onto the page stack.
void push(String location, {Object? extra}) =>
/// Push a location onto the page stack with an optional promise.
Future<T?> push<T extends Object?>(String location, {Object? extra}) =>
GoRouter.of(this).push(location, extra: extra);

/// Navigate to a named route onto the page stack.
void pushNamed(
/// Navigate to a named route onto the page stack with an optional promise.
Future<T?> pushNamed<T extends Object?>(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
Object? extra,
}) =>
GoRouter.of(this).pushNamed(
GoRouter.of(this).pushNamed<T>(
name,
params: params,
queryParams: queryParams,
Expand All @@ -59,7 +59,7 @@ extension GoRouterHelper on BuildContext {

/// Pop the top page off the Navigator's page stack by calling
/// [Navigator.pop].
void pop() => GoRouter.of(this).pop();
void pop<T extends Object?>([T? value]) => GoRouter.of(this).pop<T>(value);

/// Replaces the top-most page of the page stack with the given URL location
/// w/ optional query parameters, e.g. `/family/f2/person/p1?color=blue`.
Expand Down
48 changes: 23 additions & 25 deletions packages/go_router/lib/src/router.dart
Expand Up @@ -175,34 +175,44 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
extra: extra,
);

/// Push a URI location onto the page stack w/ optional query parameters, e.g.
/// `/family/f2/person/p1?color=blue`
void push(String location, {Object? extra}) {
/// Push a URI location onto the page stack w/ optional query parameters
/// and promise, e.g. `/family/f2/person/p1?color=blue`.
Future<T?> push<T extends Object?>(String location, {Object? extra}) async {
assert(() {
log.info('pushing $location');
return true;
}());
_routeInformationParser
.parseRouteInformation(
DebugGoRouteInformation(location: location, state: extra))
.then<void>((RouteMatchList matches) {
_routerDelegate.push(matches.last);
});
final RouteMatchList matches =
await _routeInformationParser.parseRouteInformation(
DebugGoRouteInformation(location: location, state: extra));
return _routerDelegate.push<T>(matches.last);
}

/// Push a named route onto the page stack w/ optional parameters, e.g.
/// `name='person', params={'fid': 'f2', 'pid': 'p1'}`
void pushNamed(
/// Push a named route asynchronously onto the page stack w/ optional
/// parameters and promise.
Future<T?> pushNamed<T extends Object?>(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
Object? extra,
}) =>
push(
push<T>(
namedLocation(name, params: params, queryParams: queryParams),
extra: extra,
);

/// Returns `true` if there is more than 1 page on the stack.
bool canPop() => _routerDelegate.canPop();

/// Pop the top page off the GoRouter's page stack.
void pop<T extends Object?>([T? result]) {
assert(() {
log.info('popping $location');
return true;
}());
routerDelegate.pop(result);
}

/// Replaces the top-most page of the page stack with the given URL location
/// w/ optional query parameters, e.g. `/family/f2/person/p1?color=blue`.
///
Expand Down Expand Up @@ -238,18 +248,6 @@ class GoRouter extends ChangeNotifier with NavigatorObserver {
);
}

/// Returns `true` if there is more than 1 page on the stack.
bool canPop() => _routerDelegate.canPop();

/// Pop the top page off the GoRouter's page stack.
void pop() {
assert(() {
log.info('popping $location');
return true;
}());
_routerDelegate.pop();
}

/// Refresh the route.
void refresh() {
assert(() {
Expand Down
2 changes: 1 addition & 1 deletion packages/go_router/pubspec.yaml
@@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
version: 4.2.7
version: 4.3.0
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

Expand Down
57 changes: 51 additions & 6 deletions packages/go_router/test/go_router_test.dart
Expand Up @@ -1676,7 +1676,7 @@ void main() {
title: 'GoRouter Example',
),
);
key.currentContext!.namedLocation(
key.currentContext?.namedLocation(
name,
params: params,
queryParams: queryParams,
Expand All @@ -1696,7 +1696,7 @@ void main() {
title: 'GoRouter Example',
),
);
key.currentContext!.go(
key.currentContext?.go(
location,
extra: extra,
);
Expand All @@ -1715,7 +1715,7 @@ void main() {
title: 'GoRouter Example',
),
);
key.currentContext!.goNamed(
key.currentContext?.goNamed(
name,
params: params,
queryParams: queryParams,
Expand All @@ -1738,14 +1738,34 @@ void main() {
title: 'GoRouter Example',
),
);
key.currentContext!.push(
await key.currentContext?.push(
location,
extra: extra,
);
expect(router.myLocation, location);
expect(router.extra, extra);
});

testWidgets('calls [push] on closest GoRouter with a promise',
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
testWidgets('calls [push] on closest GoRouter with a promise',
testWidgets('calls [push] on closest GoRouter with a Future',

(WidgetTester tester) async {
final GoRouterPushAsyncSpy router = GoRouterPushAsyncSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
),
);
final String? result = await key.currentContext?.push<String>(
location,
extra: extra,
);
expect(result, extra);
expect(router.myLocation, location);
expect(router.extra, extra);
});

testWidgets('calls [pushNamed] on closest GoRouter',
(WidgetTester tester) async {
final GoRouterPushNamedSpy router = GoRouterPushNamedSpy(routes: routes);
Expand All @@ -1757,16 +1777,41 @@ void main() {
title: 'GoRouter Example',
),
);
key.currentContext!.pushNamed(
await key.currentContext?.pushNamed(
name,
params: params,
queryParams: queryParams,
extra: extra,
);
expect(router.extra, extra);
expect(router.name, name);
expect(router.params, params);
expect(router.queryParams, queryParams);
});

testWidgets('calls [pushNamed] on closest GoRouter with a promise',
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
testWidgets('calls [pushNamed] on closest GoRouter with a promise',
testWidgets('calls [pushNamed] on closest GoRouter with a Future',

(WidgetTester tester) async {
final GoRouterPushNamedAsyncSpy router =
GoRouterPushNamedAsyncSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
),
);
final String? result = await key.currentContext?.pushNamed<String>(
name,
params: params,
queryParams: queryParams,
extra: extra,
);
expect(result, extra);
expect(router.extra, extra);
expect(router.name, name);
expect(router.params, params);
expect(router.queryParams, queryParams);
});

testWidgets('calls [pop] on closest GoRouter', (WidgetTester tester) async {
Expand All @@ -1779,7 +1824,7 @@ void main() {
title: 'GoRouter Example',
),
);
key.currentContext!.pop();
key.currentContext?.pop();
expect(router.popped, true);
});
});
Expand Down
3 changes: 2 additions & 1 deletion packages/go_router/test/inherited_test.dart
Expand Up @@ -129,10 +129,11 @@ class MockGoRouter extends GoRouter {
late String latestPushedName;

@override
void pushNamed(String name,
Future<T?> pushNamed<T extends Object?>(String name,
{Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
Object? extra}) {
latestPushedName = name;
return Future<T>.value();
}
}