mirror of
https://github.com/flutter/packages.git
synced 2025-07-01 07:08:10 +08:00
[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](https://github.com/flutter/flutter/issues/103659#issuecomment-1865112881). No tests were broken as a result of this change, and we have added coverage for this change through new tests.
This commit is contained in:
@ -1,3 +1,7 @@
|
||||
## 13.2.0
|
||||
|
||||
- Exposes full `Uri` on `GoRouterState` in `GoRouterRedirect`
|
||||
|
||||
## 13.1.0
|
||||
|
||||
- Adds `topRoute` to `GoRouterState`
|
||||
|
@ -11,10 +11,6 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'match.dart';
|
||||
|
||||
// TODO(chunhtai): remove this ignore and migrate the code
|
||||
// https://github.com/flutter/flutter/issues/124045.
|
||||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
/// The type of the navigation.
|
||||
///
|
||||
/// This enum is used by [RouteInformationState] to denote the navigation
|
||||
@ -85,7 +81,7 @@ class GoRouteInformationProvider extends RouteInformationProvider
|
||||
Listenable? refreshListenable,
|
||||
}) : _refreshListenable = refreshListenable,
|
||||
_value = RouteInformation(
|
||||
location: initialLocation,
|
||||
uri: Uri.parse(initialLocation),
|
||||
state: RouteInformationState<void>(
|
||||
extra: initialExtra, type: NavigatingType.go),
|
||||
),
|
||||
@ -96,8 +92,8 @@ class GoRouteInformationProvider extends RouteInformationProvider
|
||||
final Listenable? _refreshListenable;
|
||||
|
||||
static WidgetsBinding get _binding => WidgetsBinding.instance;
|
||||
static const RouteInformation _kEmptyRouteInformation =
|
||||
RouteInformation(location: '');
|
||||
static final RouteInformation _kEmptyRouteInformation =
|
||||
RouteInformation(uri: Uri.parse(''));
|
||||
|
||||
@override
|
||||
void routerReportsNewRouteInformation(RouteInformation routeInformation,
|
||||
@ -109,9 +105,9 @@ class GoRouteInformationProvider extends RouteInformationProvider
|
||||
final bool replace;
|
||||
switch (type) {
|
||||
case RouteInformationReportingType.none:
|
||||
if (_valueInEngine.location == routeInformation.location &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(_valueInEngine.state, routeInformation.state)) {
|
||||
if (!_valueHasChanged(
|
||||
newLocationUri: routeInformation.uri,
|
||||
newState: routeInformation.state)) {
|
||||
return;
|
||||
}
|
||||
replace = _valueInEngine == _kEmptyRouteInformation;
|
||||
@ -122,10 +118,7 @@ class GoRouteInformationProvider extends RouteInformationProvider
|
||||
}
|
||||
SystemNavigator.selectMultiEntryHistory();
|
||||
SystemNavigator.routeInformationUpdated(
|
||||
// TODO(chunhtai): remove this ignore and migrate the code
|
||||
// https://github.com/flutter/flutter/issues/124045.
|
||||
// ignore: unnecessary_null_checks, unnecessary_non_null_assertion
|
||||
location: routeInformation.location!,
|
||||
uri: routeInformation.uri,
|
||||
state: routeInformation.state,
|
||||
replace: replace,
|
||||
);
|
||||
@ -137,17 +130,16 @@ class GoRouteInformationProvider extends RouteInformationProvider
|
||||
RouteInformation _value;
|
||||
|
||||
@override
|
||||
// TODO(chunhtai): remove this ignore once package minimum dart version is
|
||||
// above 3.
|
||||
// ignore: unnecessary_overrides
|
||||
void notifyListeners() {
|
||||
super.notifyListeners();
|
||||
}
|
||||
|
||||
void _setValue(String location, Object state) {
|
||||
final Uri uri = Uri.parse(location);
|
||||
|
||||
final bool shouldNotify =
|
||||
_value.location != location || _value.state != state;
|
||||
_value = RouteInformation(location: location, state: state);
|
||||
_valueHasChanged(newLocationUri: uri, newState: state);
|
||||
_value = RouteInformation(uri: Uri.parse(location), state: state);
|
||||
if (shouldNotify) {
|
||||
notifyListeners();
|
||||
}
|
||||
@ -235,7 +227,7 @@ class GoRouteInformationProvider extends RouteInformationProvider
|
||||
_value = _valueInEngine = routeInformation;
|
||||
} else {
|
||||
_value = RouteInformation(
|
||||
location: routeInformation.location,
|
||||
uri: routeInformation.uri,
|
||||
state: RouteInformationState<void>(type: NavigatingType.go),
|
||||
);
|
||||
_valueInEngine = _kEmptyRouteInformation;
|
||||
@ -243,6 +235,19 @@ class GoRouteInformationProvider extends RouteInformationProvider
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool _valueHasChanged(
|
||||
{required Uri newLocationUri, required Object? newState}) {
|
||||
const DeepCollectionEquality deepCollectionEquality =
|
||||
DeepCollectionEquality();
|
||||
return !deepCollectionEquality.equals(
|
||||
_value.uri.path, newLocationUri.path) ||
|
||||
!deepCollectionEquality.equals(
|
||||
_value.uri.queryParameters, newLocationUri.queryParameters) ||
|
||||
!deepCollectionEquality.equals(
|
||||
_value.uri.fragment, newLocationUri.fragment) ||
|
||||
!deepCollectionEquality.equals(_value.state, newState);
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
if (!hasListeners) {
|
||||
@ -274,11 +279,4 @@ class GoRouteInformationProvider extends RouteInformationProvider
|
||||
_platformReportsNewRouteInformation(routeInformation);
|
||||
return SynchronousFuture<bool>(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> didPushRoute(String route) {
|
||||
assert(hasListeners);
|
||||
_platformReportsNewRouteInformation(RouteInformation(location: route));
|
||||
return SynchronousFuture<bool>(true);
|
||||
}
|
||||
}
|
||||
|
@ -79,19 +79,13 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
|
||||
}
|
||||
|
||||
late final RouteMatchList initialMatches;
|
||||
initialMatches =
|
||||
// TODO(chunhtai): remove this ignore and migrate the code
|
||||
// https://github.com/flutter/flutter/issues/124045.
|
||||
// TODO(chunhtai): After the migration from routeInformation's location
|
||||
// to uri, empty path check might be required here; see
|
||||
// https://github.com/flutter/packages/pull/5113#discussion_r1374861070
|
||||
// ignore: deprecated_member_use, unnecessary_non_null_assertion
|
||||
configuration.findMatch(routeInformation.location!, extra: state.extra);
|
||||
initialMatches = configuration.findMatch(
|
||||
routeInformation.uri.path.isEmpty
|
||||
? '${routeInformation.uri}/'
|
||||
: routeInformation.uri.toString(),
|
||||
extra: state.extra);
|
||||
if (initialMatches.isError) {
|
||||
// TODO(chunhtai): remove this ignore and migrate the code
|
||||
// https://github.com/flutter/flutter/issues/124045.
|
||||
// ignore: deprecated_member_use
|
||||
log('No initial matches: ${routeInformation.location}');
|
||||
log('No initial matches: ${routeInformation.uri.path}');
|
||||
}
|
||||
|
||||
return debugParserFuture = _redirect(
|
||||
@ -142,10 +136,7 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
|
||||
location = configuration.uri.toString();
|
||||
}
|
||||
return RouteInformation(
|
||||
// TODO(chunhtai): remove this ignore and migrate the code
|
||||
// https://github.com/flutter/flutter/issues/124045.
|
||||
// ignore: deprecated_member_use
|
||||
location: location,
|
||||
uri: Uri.parse(location),
|
||||
state: _routeMatchListCodec.encode(configuration),
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'misc/errors.dart';
|
||||
import 'route.dart';
|
||||
|
||||
final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?');
|
||||
@ -120,6 +121,9 @@ String concatenatePaths(String parentPath, String childPath) {
|
||||
|
||||
/// Normalizes the location string.
|
||||
String canonicalUri(String loc) {
|
||||
if (loc.isEmpty) {
|
||||
throw GoException('Location cannot be empty.');
|
||||
}
|
||||
String canon = Uri.parse(loc).toString();
|
||||
canon = canon.endsWith('?') ? canon.substring(0, canon.length - 1) : canon;
|
||||
|
||||
@ -131,9 +135,18 @@ String canonicalUri(String loc) {
|
||||
? canon.substring(0, canon.length - 1)
|
||||
: canon;
|
||||
|
||||
// replace '/?', except for first occurrence, from path only
|
||||
// /login/?from=/ => /login?from=/
|
||||
// /?from=/ => /?from=/
|
||||
canon = canon.replaceFirst('/?', '?', 1);
|
||||
final Uri uri = Uri.parse(canon);
|
||||
final int pathStartIndex = uri.host.isNotEmpty
|
||||
? uri.toString().indexOf(uri.host) + uri.host.length
|
||||
: uri.hasScheme
|
||||
? uri.toString().indexOf(uri.scheme) + uri.scheme.length
|
||||
: 0;
|
||||
if (pathStartIndex < canon.length) {
|
||||
canon = canon.replaceFirst('/?', '?', pathStartIndex + 1);
|
||||
}
|
||||
|
||||
return canon;
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
name: go_router
|
||||
description: A declarative router for Flutter based on Navigation 2 supporting
|
||||
deep linking, data-driven routes and more
|
||||
version: 13.1.0
|
||||
version: 13.2.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
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
flutter: ">=3.10.0"
|
||||
sdk: ">=3.1.0 <4.0.0"
|
||||
flutter: ">=3.13.0"
|
||||
|
||||
dependencies:
|
||||
collection: ^1.15.0
|
||||
|
@ -258,6 +258,95 @@ void main() {
|
||||
expect(find.byType(DummyScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'match top level route when location has scheme/host and has trailing /',
|
||||
(WidgetTester tester) async {
|
||||
final List<GoRoute> routes = <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const HomeScreen(),
|
||||
),
|
||||
];
|
||||
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('https://www.domain.com/?bar=baz');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(matches.first.matchedLocation, '/');
|
||||
expect(find.byType(HomeScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'match top level route when location has scheme/host and has trailing / (2)',
|
||||
(WidgetTester tester) async {
|
||||
final List<GoRoute> routes = <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/login',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const LoginScreen(),
|
||||
),
|
||||
];
|
||||
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('https://www.domain.com/login/');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(matches.first.matchedLocation, '/login');
|
||||
expect(find.byType(LoginScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'match top level route when location has scheme/host and has trailing / (3)',
|
||||
(WidgetTester tester) async {
|
||||
final List<GoRoute> routes = <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
builder: dummy,
|
||||
redirect: (_, __) => '/profile/foo'),
|
||||
GoRoute(path: '/profile/:kind', builder: dummy),
|
||||
];
|
||||
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('https://www.domain.com/profile/');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(matches.first.matchedLocation, '/profile/foo');
|
||||
expect(find.byType(DummyScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'match top level route when location has scheme/host and has trailing / (4)',
|
||||
(WidgetTester tester) async {
|
||||
final List<GoRoute> routes = <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
builder: dummy,
|
||||
redirect: (_, __) => '/profile/foo'),
|
||||
GoRoute(path: '/profile/:kind', builder: dummy),
|
||||
];
|
||||
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('https://www.domain.com/profile/?bar=baz');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(matches.first.matchedLocation, '/profile/foo');
|
||||
expect(find.byType(DummyScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('repeatedly pops imperative route does not crash',
|
||||
(WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/123369.
|
||||
@ -1301,10 +1390,7 @@ void main() {
|
||||
expect(find.byKey(const ValueKey<String>('home')), findsOneWidget);
|
||||
|
||||
router.routeInformationProvider.didPushRouteInformation(
|
||||
// TODO(chunhtai): remove this ignore and migrate the code
|
||||
// https://github.com/flutter/flutter/issues/124045.
|
||||
// ignore: deprecated_member_use
|
||||
RouteInformation(location: location, state: state));
|
||||
RouteInformation(uri: Uri.parse(location), state: state));
|
||||
await tester.pumpAndSettle();
|
||||
// Make sure it has all the imperative routes.
|
||||
expect(find.byKey(const ValueKey<String>('settings-1')), findsOneWidget);
|
||||
@ -2435,10 +2521,7 @@ void main() {
|
||||
routes,
|
||||
tester,
|
||||
);
|
||||
// TODO(chunhtai): remove this ignore and migrate the code
|
||||
// https://github.com/flutter/flutter/issues/124045.
|
||||
// ignore: deprecated_member_use
|
||||
expect(router.routeInformationProvider.value.location, '/dummy');
|
||||
expect(router.routeInformationProvider.value.uri.path, '/dummy');
|
||||
TestWidgetsFlutterBinding.instance.platformDispatcher
|
||||
.clearDefaultRouteNameTestValue();
|
||||
});
|
||||
|
@ -26,11 +26,46 @@ void main() {
|
||||
GoRouteInformationProvider(
|
||||
initialLocation: initialRoute, initialExtra: null);
|
||||
provider.addListener(expectAsync0(() {}));
|
||||
// TODO(chunhtai): remove this ignore and migrate the code
|
||||
// https://github.com/flutter/flutter/issues/124045.
|
||||
// ignore_for_file: deprecated_member_use
|
||||
provider
|
||||
.didPushRouteInformation(const RouteInformation(location: newRoute));
|
||||
.didPushRouteInformation(RouteInformation(uri: Uri.parse(newRoute)));
|
||||
});
|
||||
|
||||
testWidgets('didPushRouteInformation maintains uri scheme and host',
|
||||
(WidgetTester tester) async {
|
||||
const String expectedScheme = 'https';
|
||||
const String expectedHost = 'www.example.com';
|
||||
const String expectedPath = '/some/path';
|
||||
const String expectedUriString =
|
||||
'$expectedScheme://$expectedHost$expectedPath';
|
||||
late final GoRouteInformationProvider provider =
|
||||
GoRouteInformationProvider(
|
||||
initialLocation: initialRoute, initialExtra: null);
|
||||
provider.addListener(expectAsync0(() {}));
|
||||
provider.didPushRouteInformation(
|
||||
RouteInformation(uri: Uri.parse(expectedUriString)));
|
||||
expect(provider.value.uri.scheme, 'https');
|
||||
expect(provider.value.uri.host, 'www.example.com');
|
||||
expect(provider.value.uri.path, '/some/path');
|
||||
expect(provider.value.uri.toString(), expectedUriString);
|
||||
});
|
||||
|
||||
testWidgets('didPushRoute maintains uri scheme and host',
|
||||
(WidgetTester tester) async {
|
||||
const String expectedScheme = 'https';
|
||||
const String expectedHost = 'www.example.com';
|
||||
const String expectedPath = '/some/path';
|
||||
const String expectedUriString =
|
||||
'$expectedScheme://$expectedHost$expectedPath';
|
||||
late final GoRouteInformationProvider provider =
|
||||
GoRouteInformationProvider(
|
||||
initialLocation: initialRoute, initialExtra: null);
|
||||
provider.addListener(expectAsync0(() {}));
|
||||
provider.didPushRouteInformation(
|
||||
RouteInformation(uri: Uri.parse(expectedUriString)));
|
||||
expect(provider.value.uri.scheme, 'https');
|
||||
expect(provider.value.uri.host, 'www.example.com');
|
||||
expect(provider.value.uri.path, '/some/path');
|
||||
expect(provider.value.uri.toString(), expectedUriString);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -10,10 +10,7 @@ import 'test_helpers.dart';
|
||||
|
||||
RouteInformation createRouteInformation(String location, [Object? extra]) {
|
||||
return RouteInformation(
|
||||
// TODO(chunhtai): remove this ignore and migrate the code
|
||||
// https://github.com/flutter/flutter/issues/124045.
|
||||
// ignore: deprecated_member_use
|
||||
location: location,
|
||||
uri: Uri.parse(location),
|
||||
state:
|
||||
RouteInformationState<void>(type: NavigatingType.go, extra: extra));
|
||||
}
|
||||
@ -83,6 +80,52 @@ void main() {
|
||||
expect(matches[1].route, routes[0].routes[0]);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
"GoRouteInformationParser can parse deeplink route and maintain uri's scheme and host",
|
||||
(WidgetTester tester) async {
|
||||
const String expectedScheme = 'https';
|
||||
const String expectedHost = 'www.example.com';
|
||||
const String expectedPath = '/abc';
|
||||
const String expectedUriString =
|
||||
'$expectedScheme://$expectedHost$expectedPath';
|
||||
final List<GoRoute> routes = <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (_, __) => const Placeholder(),
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: 'abc',
|
||||
builder: (_, __) => const Placeholder(),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
final GoRouteInformationParser parser = await createParser(
|
||||
tester,
|
||||
routes: routes,
|
||||
redirectLimit: 100,
|
||||
redirect: (_, __) => null,
|
||||
);
|
||||
|
||||
final BuildContext context = tester.element(find.byType(Router<Object>));
|
||||
|
||||
final RouteMatchList matchesObj =
|
||||
await parser.parseRouteInformationWithDependencies(
|
||||
createRouteInformation(expectedUriString), context);
|
||||
final List<RouteMatchBase> matches = matchesObj.matches;
|
||||
expect(matches.length, 2);
|
||||
expect(matchesObj.uri.toString(), expectedUriString);
|
||||
expect(matchesObj.uri.scheme, expectedScheme);
|
||||
expect(matchesObj.uri.host, expectedHost);
|
||||
expect(matchesObj.uri.path, expectedPath);
|
||||
|
||||
expect(matches[0].matchedLocation, '/');
|
||||
expect(matches[0].route, routes[0]);
|
||||
|
||||
expect(matches[1].matchedLocation, '/abc');
|
||||
expect(matches[1].route, routes[0].routes[0]);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'GoRouteInformationParser can restore full route matches if optionURLReflectsImperativeAPIs is true',
|
||||
(WidgetTester tester) async {
|
||||
@ -114,11 +157,7 @@ void main() {
|
||||
|
||||
final RouteInformation restoredRouteInformation =
|
||||
router.routeInformationParser.restoreRouteInformation(matchList)!;
|
||||
// URL reflects the latest push.
|
||||
// TODO(chunhtai): remove this ignore and migrate the code
|
||||
// https://github.com/flutter/flutter/issues/124045.
|
||||
// ignore: deprecated_member_use
|
||||
expect(restoredRouteInformation.location, '/');
|
||||
expect(restoredRouteInformation.uri.path, '/');
|
||||
|
||||
// Can restore back to original RouteMatchList.
|
||||
final RouteMatchList parsedRouteMatch = await router.routeInformationParser
|
||||
|
Reference in New Issue
Block a user