Skip to content

[go_router] Add support for relative routes #6825

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
merged 20 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
02689ff
- Adds `GoRouter.goRelative`
ThangVuNguyenViet May 29, 2024
e8ac4ee
add go_relative example for go_router
ThangVuNguyenViet May 29, 2024
c42151c
add go_relative_test for the example
ThangVuNguyenViet May 29, 2024
022db98
fix failed test
ThangVuNguyenViet May 29, 2024
812498e
replace goRelative with go('./$path')
ThangVuNguyenViet Jun 19, 2024
07b63ad
Commit missing files during merge
ThangVuNguyenViet Jun 19, 2024
c025d86
update changelog & version
ThangVuNguyenViet Jun 19, 2024
3fb6463
Prevent concatenateUris from adding trailing redundant '?'. Add test …
ThangVuNguyenViet Jun 19, 2024
205d096
Add more `concatenateUris` test. Fix joining params
ThangVuNguyenViet Jun 19, 2024
8e4db8e
update example description
ThangVuNguyenViet Jul 10, 2024
0ead0af
Make concatenateUris not merging parameters, only take them from chil…
ThangVuNguyenViet Jul 19, 2024
9407200
Refactor example & its test
ThangVuNguyenViet Jul 19, 2024
6830539
Remove TypedRelativeGoRoute
ThangVuNguyenViet Sep 6, 2024
43eb497
Add missing import
ThangVuNguyenViet Sep 6, 2024
2dd80d9
add fragment test to concatenateUris
ThangVuNguyenViet Sep 24, 2024
3071a53
Merge branch 'main' into go_router/go-relative
ThangVuNguyenViet Sep 24, 2024
4dba574
Merge branch 'main' of github.com:flutter/packages into go_router/go-…
ThangVuNguyenViet Nov 3, 2024
fb61d97
Merge branch 'main' into go_router/go-relative
ThangVuNguyenViet Nov 7, 2024
d1ebfeb
bump version to 14.4.2 in pubspec.yaml
ThangVuNguyenViet Nov 8, 2024
c246bc3
Merge branch 'main' into go_router/go-relative
chunhtai Nov 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 14.6.0

- Allows going to a path relatively by prefixing `./`

## 14.5.0

- Adds preload support to StatefulShellRoute, configurable via `preload` parameter on StatefulShellBranch.
Expand Down
128 changes: 128 additions & 0 deletions packages/go_router/example/lib/go_relative.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

/// This sample app demonstrates how to use go relatively with GoRouter.go('./$path').
void main() => runApp(const MyApp());

/// The main app.
class MyApp extends StatelessWidget {
/// Constructs a [MyApp]
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}

/// The route configuration.
final GoRouter _router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const HomeScreen();
},
routes: <RouteBase>[
GoRoute(
path: 'details',
builder: (BuildContext context, GoRouterState state) {
return const DetailsScreen();
},
routes: <RouteBase>[
GoRoute(
path: 'settings',
builder: (BuildContext context, GoRouterState state) {
return const SettingsScreen();
},
),
],
),
],
),
],
);

/// The home screen
class HomeScreen extends StatelessWidget {
/// Constructs a [HomeScreen]
const HomeScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home Screen')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () => context.go('./details'),
child: const Text('Go to the Details screen'),
),
],
),
),
);
}
}

/// The details screen
class DetailsScreen extends StatelessWidget {
/// Constructs a [DetailsScreen]
const DetailsScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details Screen')),
body: Center(
child: Column(
children: <Widget>[
TextButton(
onPressed: () {
context.pop();
},
child: const Text('Go back'),
),
TextButton(
onPressed: () {
context.go('./settings');
},
child: const Text('Go to the Settings screen'),
),
],
),
),
);
}
}

/// The settings screen
class SettingsScreen extends StatelessWidget {
/// Constructs a [SettingsScreen]
const SettingsScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Settings Screen')),
body: Column(
children: <Widget>[
TextButton(
onPressed: () {
context.pop();
},
child: const Text('Go back'),
),
],
),
);
}
}
29 changes: 29 additions & 0 deletions packages/go_router/example/test/go_relative_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter_test/flutter_test.dart';
import 'package:go_router_examples/go_relative.dart' as example;

void main() {
testWidgets('example works', (WidgetTester tester) async {
await tester.pumpWidget(const example.MyApp());
expect(find.byType(example.HomeScreen), findsOneWidget);

await tester.tap(find.text('Go to the Details screen'));
await tester.pumpAndSettle();
expect(find.byType(example.DetailsScreen), findsOneWidget);

await tester.tap(find.text('Go to the Settings screen'));
await tester.pumpAndSettle();
expect(find.byType(example.SettingsScreen), findsOneWidget);

await tester.tap(find.text('Go back'));
await tester.pumpAndSettle();
expect(find.byType(example.DetailsScreen), findsOneWidget);

await tester.tap(find.text('Go back'));
await tester.pumpAndSettle();
expect(find.byType(example.HomeScreen), findsOneWidget);
});
}
10 changes: 8 additions & 2 deletions packages/go_router/lib/src/information_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

import 'match.dart';
import 'path_utils.dart';

/// The type of the navigation.
///
Expand Down Expand Up @@ -139,11 +140,16 @@ class GoRouteInformationProvider extends RouteInformationProvider
}

void _setValue(String location, Object state) {
final Uri uri = Uri.parse(location);
Uri uri = Uri.parse(location);

// Check for relative location
if (location.startsWith('./')) {
uri = concatenateUris(_value.uri, uri);
}

final bool shouldNotify =
_valueHasChanged(newLocationUri: uri, newState: state);
_value = RouteInformation(uri: Uri.parse(location), state: state);
_value = RouteInformation(uri: uri, state: state);
if (shouldNotify) {
notifyListeners();
}
Expand Down
49 changes: 49 additions & 0 deletions packages/go_router/lib/src/path_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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+)(\((?:\\.|[^\\()])+\))?');
Expand Down Expand Up @@ -112,6 +113,54 @@ String concatenatePaths(String parentPath, String childPath) {
return '/${segments.join('/')}';
}

/// Concatenates two Uri. It will [concatenatePaths] the parent's and the child's paths, and take only the child's parameters.
///
/// e.g: pathA = /a?fid=f1, pathB = c/d?pid=p2, concatenatePaths(pathA, pathB) = /a/c/d?pid=2.
Uri concatenateUris(Uri parentUri, Uri childUri) {
Copy link
Contributor

Choose a reason for hiding this comment

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

the path looks good, but I am a bit hesitate on also merge query parameter. It has different logic from regular go. Regular go will not attempt to merge with current query parameters. I think we should probably follow the same logic to avoid confusion.

Choose a reason for hiding this comment

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

Hmm this is quite interesting thing thou. Take this example.

We're in /family/f2?sort=desc and we're going to the inner person/p1 route, and we'll pop it to go back to the family route. How are devs handling this case?

In go_router_builder, devs can't use the easy PersonRoute(pid: 'p1').go but rather have to get its route.location, convert to Uri, and then manually add the sort param. Being able to use Route().go in builder is kinda a big selling point of the package imo

Copy link
Contributor

Choose a reason for hiding this comment

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

you can access GoRouterState.of(context).uri.queryParemeters.

Also won't in this case PersonRoute also declare sort parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

make sense. I've been doing things wrong in my app then

Uri newUri = childUri.replace(
path: concatenatePaths(parentUri.path, childUri.path),
);

// Parse the new normalized uri to remove unnecessary parts, like the trailing '?'.
newUri = Uri.parse(canonicalUri(newUri.toString()));
return newUri;
}

/// 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;
final Uri uri = Uri.parse(canon);

// remove trailing slash except for when you shouldn't, e.g.
// /profile/ => /profile
// / => /
// /login?from=/ => /login?from=/
canon = uri.path.endsWith('/') &&
uri.path != '/' &&
!uri.hasQuery &&
!uri.hasFragment
? canon.substring(0, canon.length - 1)
: canon;

// replace '/?', except for first occurrence, from path only
// /login/?from=/ => /login?from=/
// /?from=/ => /?from=/
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;
}

/// Builds an absolute path for the provided route.
String? fullPathForRoute(
RouteBase targetRoute, String parentFullpath, List<RouteBase> routes) {
Expand Down
2 changes: 1 addition & 1 deletion packages/go_router/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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: 14.5.0
version: 14.6.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
Loading