Skip to content

Commit c269727

Browse files
authored
[go_router] Expose full Uri on GoRouterState in GoRouterRedirect (#5742)
A number of developers have voiced the desire to be able to know whether a `GoRouterState` is the result of a deep-link or in-app navigation from within their `GoRouter`'s `redirect` method. This can be accomplished by exposing the `Uri`'s `scheme` and `host` on the `GoRouterState`. This way, we can know whether the `GoRouterState` is the result of a deep-link by checking `state.uri.scheme != null` or `state.uri.host != null`. This PR would close [#103659](flutter/flutter#103659 (comment)). No tests were broken as a result of this change, and we have added coverage for this change through new tests.
1 parent fe7d52d commit c269727

8 files changed

+231
-68
lines changed

packages/go_router/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 13.2.0
2+
3+
- Exposes full `Uri` on `GoRouterState` in `GoRouterRedirect`
4+
15
## 13.1.0
26

37
- Adds `topRoute` to `GoRouterState`

packages/go_router/lib/src/information_provider.dart

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ import 'package:flutter/widgets.dart';
1111

1212
import 'match.dart';
1313

14-
// TODO(chunhtai): remove this ignore and migrate the code
15-
// https://github.com/flutter/flutter/issues/124045.
16-
// ignore_for_file: deprecated_member_use
17-
1814
/// The type of the navigation.
1915
///
2016
/// This enum is used by [RouteInformationState] to denote the navigation
@@ -85,7 +81,7 @@ class GoRouteInformationProvider extends RouteInformationProvider
8581
Listenable? refreshListenable,
8682
}) : _refreshListenable = refreshListenable,
8783
_value = RouteInformation(
88-
location: initialLocation,
84+
uri: Uri.parse(initialLocation),
8985
state: RouteInformationState<void>(
9086
extra: initialExtra, type: NavigatingType.go),
9187
),
@@ -96,8 +92,8 @@ class GoRouteInformationProvider extends RouteInformationProvider
9692
final Listenable? _refreshListenable;
9793

9894
static WidgetsBinding get _binding => WidgetsBinding.instance;
99-
static const RouteInformation _kEmptyRouteInformation =
100-
RouteInformation(location: '');
95+
static final RouteInformation _kEmptyRouteInformation =
96+
RouteInformation(uri: Uri.parse(''));
10197

10298
@override
10399
void routerReportsNewRouteInformation(RouteInformation routeInformation,
@@ -109,9 +105,9 @@ class GoRouteInformationProvider extends RouteInformationProvider
109105
final bool replace;
110106
switch (type) {
111107
case RouteInformationReportingType.none:
112-
if (_valueInEngine.location == routeInformation.location &&
113-
const DeepCollectionEquality()
114-
.equals(_valueInEngine.state, routeInformation.state)) {
108+
if (!_valueHasChanged(
109+
newLocationUri: routeInformation.uri,
110+
newState: routeInformation.state)) {
115111
return;
116112
}
117113
replace = _valueInEngine == _kEmptyRouteInformation;
@@ -122,10 +118,7 @@ class GoRouteInformationProvider extends RouteInformationProvider
122118
}
123119
SystemNavigator.selectMultiEntryHistory();
124120
SystemNavigator.routeInformationUpdated(
125-
// TODO(chunhtai): remove this ignore and migrate the code
126-
// https://github.com/flutter/flutter/issues/124045.
127-
// ignore: unnecessary_null_checks, unnecessary_non_null_assertion
128-
location: routeInformation.location!,
121+
uri: routeInformation.uri,
129122
state: routeInformation.state,
130123
replace: replace,
131124
);
@@ -137,17 +130,16 @@ class GoRouteInformationProvider extends RouteInformationProvider
137130
RouteInformation _value;
138131

139132
@override
140-
// TODO(chunhtai): remove this ignore once package minimum dart version is
141-
// above 3.
142-
// ignore: unnecessary_overrides
143133
void notifyListeners() {
144134
super.notifyListeners();
145135
}
146136

147137
void _setValue(String location, Object state) {
138+
final Uri uri = Uri.parse(location);
139+
148140
final bool shouldNotify =
149-
_value.location != location || _value.state != state;
150-
_value = RouteInformation(location: location, state: state);
141+
_valueHasChanged(newLocationUri: uri, newState: state);
142+
_value = RouteInformation(uri: Uri.parse(location), state: state);
151143
if (shouldNotify) {
152144
notifyListeners();
153145
}
@@ -235,14 +227,27 @@ class GoRouteInformationProvider extends RouteInformationProvider
235227
_value = _valueInEngine = routeInformation;
236228
} else {
237229
_value = RouteInformation(
238-
location: routeInformation.location,
230+
uri: routeInformation.uri,
239231
state: RouteInformationState<void>(type: NavigatingType.go),
240232
);
241233
_valueInEngine = _kEmptyRouteInformation;
242234
}
243235
notifyListeners();
244236
}
245237

238+
bool _valueHasChanged(
239+
{required Uri newLocationUri, required Object? newState}) {
240+
const DeepCollectionEquality deepCollectionEquality =
241+
DeepCollectionEquality();
242+
return !deepCollectionEquality.equals(
243+
_value.uri.path, newLocationUri.path) ||
244+
!deepCollectionEquality.equals(
245+
_value.uri.queryParameters, newLocationUri.queryParameters) ||
246+
!deepCollectionEquality.equals(
247+
_value.uri.fragment, newLocationUri.fragment) ||
248+
!deepCollectionEquality.equals(_value.state, newState);
249+
}
250+
246251
@override
247252
void addListener(VoidCallback listener) {
248253
if (!hasListeners) {
@@ -274,11 +279,4 @@ class GoRouteInformationProvider extends RouteInformationProvider
274279
_platformReportsNewRouteInformation(routeInformation);
275280
return SynchronousFuture<bool>(true);
276281
}
277-
278-
@override
279-
Future<bool> didPushRoute(String route) {
280-
assert(hasListeners);
281-
_platformReportsNewRouteInformation(RouteInformation(location: route));
282-
return SynchronousFuture<bool>(true);
283-
}
284282
}

packages/go_router/lib/src/parser.dart

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,13 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
7979
}
8080

8181
late final RouteMatchList initialMatches;
82-
initialMatches =
83-
// TODO(chunhtai): remove this ignore and migrate the code
84-
// https://github.com/flutter/flutter/issues/124045.
85-
// TODO(chunhtai): After the migration from routeInformation's location
86-
// to uri, empty path check might be required here; see
87-
// https://github.com/flutter/packages/pull/5113#discussion_r1374861070
88-
// ignore: deprecated_member_use, unnecessary_non_null_assertion
89-
configuration.findMatch(routeInformation.location!, extra: state.extra);
82+
initialMatches = configuration.findMatch(
83+
routeInformation.uri.path.isEmpty
84+
? '${routeInformation.uri}/'
85+
: routeInformation.uri.toString(),
86+
extra: state.extra);
9087
if (initialMatches.isError) {
91-
// TODO(chunhtai): remove this ignore and migrate the code
92-
// https://github.com/flutter/flutter/issues/124045.
93-
// ignore: deprecated_member_use
94-
log('No initial matches: ${routeInformation.location}');
88+
log('No initial matches: ${routeInformation.uri.path}');
9589
}
9690

9791
return debugParserFuture = _redirect(
@@ -142,10 +136,7 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
142136
location = configuration.uri.toString();
143137
}
144138
return RouteInformation(
145-
// TODO(chunhtai): remove this ignore and migrate the code
146-
// https://github.com/flutter/flutter/issues/124045.
147-
// ignore: deprecated_member_use
148-
location: location,
139+
uri: Uri.parse(location),
149140
state: _routeMatchListCodec.encode(configuration),
150141
);
151142
}

packages/go_router/lib/src/path_utils.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'misc/errors.dart';
56
import 'route.dart';
67

78
final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?');
@@ -120,6 +121,9 @@ String concatenatePaths(String parentPath, String childPath) {
120121

121122
/// Normalizes the location string.
122123
String canonicalUri(String loc) {
124+
if (loc.isEmpty) {
125+
throw GoException('Location cannot be empty.');
126+
}
123127
String canon = Uri.parse(loc).toString();
124128
canon = canon.endsWith('?') ? canon.substring(0, canon.length - 1) : canon;
125129

@@ -131,9 +135,18 @@ String canonicalUri(String loc) {
131135
? canon.substring(0, canon.length - 1)
132136
: canon;
133137

138+
// replace '/?', except for first occurrence, from path only
134139
// /login/?from=/ => /login?from=/
135140
// /?from=/ => /?from=/
136-
canon = canon.replaceFirst('/?', '?', 1);
141+
final Uri uri = Uri.parse(canon);
142+
final int pathStartIndex = uri.host.isNotEmpty
143+
? uri.toString().indexOf(uri.host) + uri.host.length
144+
: uri.hasScheme
145+
? uri.toString().indexOf(uri.scheme) + uri.scheme.length
146+
: 0;
147+
if (pathStartIndex < canon.length) {
148+
canon = canon.replaceFirst('/?', '?', pathStartIndex + 1);
149+
}
137150

138151
return canon;
139152
}

packages/go_router/pubspec.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
name: go_router
22
description: A declarative router for Flutter based on Navigation 2 supporting
33
deep linking, data-driven routes and more
4-
version: 13.1.0
4+
version: 13.2.0
55
repository: https://github.com/flutter/packages/tree/main/packages/go_router
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
77

88
environment:
9-
sdk: ">=3.0.0 <4.0.0"
10-
flutter: ">=3.10.0"
9+
sdk: ">=3.1.0 <4.0.0"
10+
flutter: ">=3.13.0"
1111

1212
dependencies:
1313
collection: ^1.15.0

packages/go_router/test/go_router_test.dart

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,95 @@ void main() {
258258
expect(find.byType(DummyScreen), findsOneWidget);
259259
});
260260

261+
testWidgets(
262+
'match top level route when location has scheme/host and has trailing /',
263+
(WidgetTester tester) async {
264+
final List<GoRoute> routes = <GoRoute>[
265+
GoRoute(
266+
path: '/',
267+
builder: (BuildContext context, GoRouterState state) =>
268+
const HomeScreen(),
269+
),
270+
];
271+
272+
final GoRouter router = await createRouter(routes, tester);
273+
router.go('https://www.domain.com/?bar=baz');
274+
await tester.pumpAndSettle();
275+
final List<RouteMatchBase> matches =
276+
router.routerDelegate.currentConfiguration.matches;
277+
expect(matches, hasLength(1));
278+
expect(matches.first.matchedLocation, '/');
279+
expect(find.byType(HomeScreen), findsOneWidget);
280+
});
281+
282+
testWidgets(
283+
'match top level route when location has scheme/host and has trailing / (2)',
284+
(WidgetTester tester) async {
285+
final List<GoRoute> routes = <GoRoute>[
286+
GoRoute(
287+
path: '/',
288+
builder: (BuildContext context, GoRouterState state) =>
289+
const HomeScreen(),
290+
),
291+
GoRoute(
292+
path: '/login',
293+
builder: (BuildContext context, GoRouterState state) =>
294+
const LoginScreen(),
295+
),
296+
];
297+
298+
final GoRouter router = await createRouter(routes, tester);
299+
router.go('https://www.domain.com/login/');
300+
await tester.pumpAndSettle();
301+
final List<RouteMatchBase> matches =
302+
router.routerDelegate.currentConfiguration.matches;
303+
expect(matches, hasLength(1));
304+
expect(matches.first.matchedLocation, '/login');
305+
expect(find.byType(LoginScreen), findsOneWidget);
306+
});
307+
308+
testWidgets(
309+
'match top level route when location has scheme/host and has trailing / (3)',
310+
(WidgetTester tester) async {
311+
final List<GoRoute> routes = <GoRoute>[
312+
GoRoute(
313+
path: '/profile',
314+
builder: dummy,
315+
redirect: (_, __) => '/profile/foo'),
316+
GoRoute(path: '/profile/:kind', builder: dummy),
317+
];
318+
319+
final GoRouter router = await createRouter(routes, tester);
320+
router.go('https://www.domain.com/profile/');
321+
await tester.pumpAndSettle();
322+
final List<RouteMatchBase> matches =
323+
router.routerDelegate.currentConfiguration.matches;
324+
expect(matches, hasLength(1));
325+
expect(matches.first.matchedLocation, '/profile/foo');
326+
expect(find.byType(DummyScreen), findsOneWidget);
327+
});
328+
329+
testWidgets(
330+
'match top level route when location has scheme/host and has trailing / (4)',
331+
(WidgetTester tester) async {
332+
final List<GoRoute> routes = <GoRoute>[
333+
GoRoute(
334+
path: '/profile',
335+
builder: dummy,
336+
redirect: (_, __) => '/profile/foo'),
337+
GoRoute(path: '/profile/:kind', builder: dummy),
338+
];
339+
340+
final GoRouter router = await createRouter(routes, tester);
341+
router.go('https://www.domain.com/profile/?bar=baz');
342+
await tester.pumpAndSettle();
343+
final List<RouteMatchBase> matches =
344+
router.routerDelegate.currentConfiguration.matches;
345+
expect(matches, hasLength(1));
346+
expect(matches.first.matchedLocation, '/profile/foo');
347+
expect(find.byType(DummyScreen), findsOneWidget);
348+
});
349+
261350
testWidgets('repeatedly pops imperative route does not crash',
262351
(WidgetTester tester) async {
263352
// Regression test for https://github.com/flutter/flutter/issues/123369.
@@ -1301,10 +1390,7 @@ void main() {
13011390
expect(find.byKey(const ValueKey<String>('home')), findsOneWidget);
13021391

13031392
router.routeInformationProvider.didPushRouteInformation(
1304-
// TODO(chunhtai): remove this ignore and migrate the code
1305-
// https://github.com/flutter/flutter/issues/124045.
1306-
// ignore: deprecated_member_use
1307-
RouteInformation(location: location, state: state));
1393+
RouteInformation(uri: Uri.parse(location), state: state));
13081394
await tester.pumpAndSettle();
13091395
// Make sure it has all the imperative routes.
13101396
expect(find.byKey(const ValueKey<String>('settings-1')), findsOneWidget);
@@ -2435,10 +2521,7 @@ void main() {
24352521
routes,
24362522
tester,
24372523
);
2438-
// TODO(chunhtai): remove this ignore and migrate the code
2439-
// https://github.com/flutter/flutter/issues/124045.
2440-
// ignore: deprecated_member_use
2441-
expect(router.routeInformationProvider.value.location, '/dummy');
2524+
expect(router.routeInformationProvider.value.uri.path, '/dummy');
24422525
TestWidgetsFlutterBinding.instance.platformDispatcher
24432526
.clearDefaultRouteNameTestValue();
24442527
});

packages/go_router/test/information_provider_test.dart

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,46 @@ void main() {
2626
GoRouteInformationProvider(
2727
initialLocation: initialRoute, initialExtra: null);
2828
provider.addListener(expectAsync0(() {}));
29-
// TODO(chunhtai): remove this ignore and migrate the code
30-
// https://github.com/flutter/flutter/issues/124045.
31-
// ignore_for_file: deprecated_member_use
3229
provider
33-
.didPushRouteInformation(const RouteInformation(location: newRoute));
30+
.didPushRouteInformation(RouteInformation(uri: Uri.parse(newRoute)));
31+
});
32+
33+
testWidgets('didPushRouteInformation maintains uri scheme and host',
34+
(WidgetTester tester) async {
35+
const String expectedScheme = 'https';
36+
const String expectedHost = 'www.example.com';
37+
const String expectedPath = '/some/path';
38+
const String expectedUriString =
39+
'$expectedScheme://$expectedHost$expectedPath';
40+
late final GoRouteInformationProvider provider =
41+
GoRouteInformationProvider(
42+
initialLocation: initialRoute, initialExtra: null);
43+
provider.addListener(expectAsync0(() {}));
44+
provider.didPushRouteInformation(
45+
RouteInformation(uri: Uri.parse(expectedUriString)));
46+
expect(provider.value.uri.scheme, 'https');
47+
expect(provider.value.uri.host, 'www.example.com');
48+
expect(provider.value.uri.path, '/some/path');
49+
expect(provider.value.uri.toString(), expectedUriString);
50+
});
51+
52+
testWidgets('didPushRoute maintains uri scheme and host',
53+
(WidgetTester tester) async {
54+
const String expectedScheme = 'https';
55+
const String expectedHost = 'www.example.com';
56+
const String expectedPath = '/some/path';
57+
const String expectedUriString =
58+
'$expectedScheme://$expectedHost$expectedPath';
59+
late final GoRouteInformationProvider provider =
60+
GoRouteInformationProvider(
61+
initialLocation: initialRoute, initialExtra: null);
62+
provider.addListener(expectAsync0(() {}));
63+
provider.didPushRouteInformation(
64+
RouteInformation(uri: Uri.parse(expectedUriString)));
65+
expect(provider.value.uri.scheme, 'https');
66+
expect(provider.value.uri.host, 'www.example.com');
67+
expect(provider.value.uri.path, '/some/path');
68+
expect(provider.value.uri.toString(), expectedUriString);
3469
});
3570
});
3671
}

0 commit comments

Comments
 (0)