Skip to content

Commit e890f6b

Browse files
authored
[go_router] Adds an ability to add a custom codec for serializing/des… (flutter#5288)
�erializing extra fixes flutter#99099 fixes flutter#137248
1 parent cccf5d2 commit e890f6b

File tree

14 files changed

+404
-21
lines changed

14 files changed

+404
-21
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+
## 12.1.0
2+
3+
- Adds an ability to add a custom codec for serializing/deserializing extra.
4+
15
## 12.0.3
26

37
- Fixes crashes when dynamically updates routing tables with named routes.

packages/go_router/doc/navigation.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,25 @@ Returning a value:
8484
onTap: () => context.pop(true)
8585
```
8686

87+
## Using extra
88+
You can provide additional data along with navigation.
89+
90+
```dart
91+
context.go('/123, extra: 'abc');
92+
```
93+
94+
and retrieve the data from GoRouterState
95+
96+
```dart
97+
final String extraString = GoRouterState.of(context).extra! as String;
98+
```
99+
100+
The extra data will go through serialization when it is stored in the browser.
101+
If you plan to use complex data as extra, consider also providing a codec
102+
to GoRouter so that it won't get dropped during serialization.
103+
104+
For an example on how to use complex data in extra with a codec, see
105+
[extra_codec.dart](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart).
106+
87107

88108
[Named routes]: https://pub.dev/documentation/go_router/latest/topics/Named%20routes-topic.html

packages/go_router/example/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ An example to demonstrate how to use a `StatefulShellRoute` to create stateful n
4141

4242
An example to demonstrate how to handle exception in go_router.
4343

44+
## [Extra Codec](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart)
45+
`flutter run lib/extra_codec.dart`
46+
47+
An example to demonstrate how to use a complex object as extra.
48+
4449
## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books)
4550
`flutter run lib/books/main.dart`
4651

packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@
156156
97C146E61CF9000F007C117D /* Project object */ = {
157157
isa = PBXProject;
158158
attributes = {
159-
LastUpgradeCheck = 1300;
159+
LastUpgradeCheck = 1430;
160160
ORGANIZATIONNAME = "";
161161
TargetAttributes = {
162162
97C146ED1CF9000F007C117D = {

packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1300"
3+
LastUpgradeVersion = "1430"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:flutter/material.dart';
8+
import 'package:go_router/go_router.dart';
9+
10+
/// This sample app demonstrates how to provide a codec for complex extra data.
11+
void main() => runApp(const MyApp());
12+
13+
/// The router configuration.
14+
final GoRouter _router = GoRouter(
15+
routes: <RouteBase>[
16+
GoRoute(
17+
path: '/',
18+
builder: (BuildContext context, GoRouterState state) =>
19+
const HomeScreen(),
20+
),
21+
],
22+
extraCodec: const MyExtraCodec(),
23+
);
24+
25+
/// The main app.
26+
class MyApp extends StatelessWidget {
27+
/// Constructs a [MyApp]
28+
const MyApp({super.key});
29+
30+
@override
31+
Widget build(BuildContext context) {
32+
return MaterialApp.router(
33+
routerConfig: _router,
34+
);
35+
}
36+
}
37+
38+
/// The home screen.
39+
class HomeScreen extends StatelessWidget {
40+
/// Constructs a [HomeScreen].
41+
const HomeScreen({super.key});
42+
43+
@override
44+
Widget build(BuildContext context) {
45+
return Scaffold(
46+
appBar: AppBar(title: const Text('Home Screen')),
47+
body: Center(
48+
child: Column(
49+
mainAxisAlignment: MainAxisAlignment.center,
50+
children: <Widget>[
51+
const Text(
52+
"If running in web, use the browser's backward and forward button to test extra codec after setting extra several times."),
53+
Text(
54+
'The extra for this page is: ${GoRouterState.of(context).extra}'),
55+
ElevatedButton(
56+
onPressed: () => context.go('/', extra: ComplexData1('data')),
57+
child: const Text('Set extra to ComplexData1'),
58+
),
59+
ElevatedButton(
60+
onPressed: () => context.go('/', extra: ComplexData2('data')),
61+
child: const Text('Set extra to ComplexData2'),
62+
),
63+
],
64+
),
65+
),
66+
);
67+
}
68+
}
69+
70+
/// A complex class.
71+
class ComplexData1 {
72+
/// Create a complex object.
73+
ComplexData1(this.data);
74+
75+
/// The data.
76+
final String data;
77+
78+
@override
79+
String toString() => 'ComplexData1(data: $data)';
80+
}
81+
82+
/// A complex class.
83+
class ComplexData2 {
84+
/// Create a complex object.
85+
ComplexData2(this.data);
86+
87+
/// The data.
88+
final String data;
89+
90+
@override
91+
String toString() => 'ComplexData2(data: $data)';
92+
}
93+
94+
/// A codec that can serialize both [ComplexData1] and [ComplexData2].
95+
class MyExtraCodec extends Codec<Object?, Object?> {
96+
/// Create a codec.
97+
const MyExtraCodec();
98+
@override
99+
Converter<Object?, Object?> get decoder => const _MyExtraDecoder();
100+
101+
@override
102+
Converter<Object?, Object?> get encoder => const _MyExtraEncoder();
103+
}
104+
105+
class _MyExtraDecoder extends Converter<Object?, Object?> {
106+
const _MyExtraDecoder();
107+
@override
108+
Object? convert(Object? input) {
109+
if (input == null) {
110+
return null;
111+
}
112+
final List<Object?> inputAsList = input as List<Object?>;
113+
if (inputAsList[0] == 'ComplexData1') {
114+
return ComplexData1(inputAsList[1]! as String);
115+
}
116+
if (inputAsList[0] == 'ComplexData2') {
117+
return ComplexData2(inputAsList[1]! as String);
118+
}
119+
throw FormatException('Unable tp parse input: $input');
120+
}
121+
}
122+
123+
class _MyExtraEncoder extends Converter<Object?, Object?> {
124+
const _MyExtraEncoder();
125+
@override
126+
Object? convert(Object? input) {
127+
if (input == null) {
128+
return null;
129+
}
130+
switch (input.runtimeType) {
131+
case ComplexData1:
132+
return <Object?>['ComplexData1', (input as ComplexData1).data];
133+
case ComplexData2:
134+
return <Object?>['ComplexData2', (input as ComplexData2).data];
135+
default:
136+
throw FormatException('Cannot encode type ${input.runtimeType}');
137+
}
138+
}
139+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter_test/flutter_test.dart';
6+
import 'package:go_router_examples/extra_codec.dart' as example;
7+
8+
void main() {
9+
testWidgets('example works', (WidgetTester tester) async {
10+
await tester.pumpWidget(const example.MyApp());
11+
expect(find.text('The extra for this page is: null'), findsOneWidget);
12+
13+
await tester.tap(find.text('Set extra to ComplexData1'));
14+
await tester.pumpAndSettle();
15+
expect(find.text('The extra for this page is: ComplexData1(data: data)'),
16+
findsOneWidget);
17+
18+
await tester.tap(find.text('Set extra to ComplexData2'));
19+
await tester.pumpAndSettle();
20+
expect(find.text('The extra for this page is: ComplexData2(data: data)'),
21+
findsOneWidget);
22+
});
23+
}

packages/go_router/lib/src/configuration.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67

78
import 'package:flutter/foundation.dart';
89
import 'package:flutter/widgets.dart';
@@ -25,6 +26,7 @@ class RouteConfiguration {
2526
RouteConfiguration(
2627
this._routingConfig, {
2728
required this.navigatorKey,
29+
this.extraCodec,
2830
}) {
2931
_onRoutingTableChanged();
3032
_routingConfig.addListener(_onRoutingTableChanged);
@@ -232,6 +234,19 @@ class RouteConfiguration {
232234
/// The global key for top level navigator.
233235
final GlobalKey<NavigatorState> navigatorKey;
234236

237+
/// The codec used to encode and decode extra into a serializable format.
238+
///
239+
/// When navigating using [GoRouter.go] or [GoRouter.push], one can provide
240+
/// an `extra` parameter along with it. If the extra contains complex data,
241+
/// consider provide a codec for serializing and deserializing the extra data.
242+
///
243+
/// See also:
244+
/// * [Navigation](https://pub.dev/documentation/go_router/latest/topics/Navigation-topic.html)
245+
/// topic.
246+
/// * [extra_codec](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart)
247+
/// example.
248+
final Codec<Object?, Object?>? extraCodec;
249+
235250
final Map<String, String> _nameToPath = <String, String>{};
236251

237252
/// Looks up the url location by a [GoRoute]'s name.

packages/go_router/lib/src/logging.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ final Logger logger = Logger('GoRouter');
1616
bool _enabled = false;
1717

1818
/// Logs the message if logging is enabled.
19-
void log(String message) {
19+
void log(String message, {Level level = Level.INFO}) {
2020
if (_enabled) {
21-
logger.info(message);
21+
logger.log(level, message);
2222
}
2323
}
2424

packages/go_router/lib/src/match.dart

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import 'dart:convert';
88
import 'package:collection/collection.dart';
99
import 'package:flutter/foundation.dart';
1010
import 'package:flutter/widgets.dart';
11+
import 'package:logging/logging.dart';
1112
import 'package:meta/meta.dart';
1213

1314
import 'configuration.dart';
15+
import 'logging.dart';
1416
import 'misc/errors.dart';
1517
import 'path_utils.dart';
1618
import 'route.dart';
@@ -358,29 +360,35 @@ class RouteMatchList {
358360
/// Handles encoding and decoding of [RouteMatchList] objects to a format
359361
/// suitable for using with [StandardMessageCodec].
360362
///
361-
/// The primary use of this class is for state restoration.
363+
/// The primary use of this class is for state restoration and browser history.
362364
@internal
363365
class RouteMatchListCodec extends Codec<RouteMatchList, Map<Object?, Object?>> {
364366
/// Creates a new [RouteMatchListCodec] object.
365367
RouteMatchListCodec(RouteConfiguration configuration)
366-
: decoder = _RouteMatchListDecoder(configuration);
368+
: decoder = _RouteMatchListDecoder(configuration),
369+
encoder = _RouteMatchListEncoder(configuration);
367370

368371
static const String _locationKey = 'location';
369372
static const String _extraKey = 'state';
370373
static const String _imperativeMatchesKey = 'imperativeMatches';
371374
static const String _pageKey = 'pageKey';
375+
static const String _codecKey = 'codec';
376+
static const String _jsonCodecName = 'json';
377+
static const String _customCodecName = 'custom';
378+
static const String _encodedKey = 'encoded';
372379

373380
@override
374-
final Converter<RouteMatchList, Map<Object?, Object?>> encoder =
375-
const _RouteMatchListEncoder();
381+
final Converter<RouteMatchList, Map<Object?, Object?>> encoder;
376382

377383
@override
378384
final Converter<Map<Object?, Object?>, RouteMatchList> decoder;
379385
}
380386

381387
class _RouteMatchListEncoder
382388
extends Converter<RouteMatchList, Map<Object?, Object?>> {
383-
const _RouteMatchListEncoder();
389+
const _RouteMatchListEncoder(this.configuration);
390+
391+
final RouteConfiguration configuration;
384392
@override
385393
Map<Object?, Object?> convert(RouteMatchList input) {
386394
final List<Map<Object?, Object?>> imperativeMatches = input.matches
@@ -394,15 +402,36 @@ class _RouteMatchListEncoder
394402
imperativeMatches: imperativeMatches);
395403
}
396404

397-
static Map<Object?, Object?> _toPrimitives(String location, Object? extra,
405+
Map<Object?, Object?> _toPrimitives(String location, Object? extra,
398406
{List<Map<Object?, Object?>>? imperativeMatches, String? pageKey}) {
399-
String? encodedExtra;
400-
try {
401-
encodedExtra = json.encoder.convert(extra);
402-
} on JsonUnsupportedObjectError {/* give up if not serializable */}
407+
Map<String, Object?> encodedExtra;
408+
if (configuration.extraCodec != null) {
409+
encodedExtra = <String, Object?>{
410+
RouteMatchListCodec._codecKey: RouteMatchListCodec._customCodecName,
411+
RouteMatchListCodec._encodedKey:
412+
configuration.extraCodec?.encode(extra),
413+
};
414+
} else {
415+
String jsonEncodedExtra;
416+
try {
417+
jsonEncodedExtra = json.encoder.convert(extra);
418+
} on JsonUnsupportedObjectError {
419+
jsonEncodedExtra = json.encoder.convert(null);
420+
log(
421+
'An extra with complex data type ${extra.runtimeType} is provided '
422+
'without a codec. Consider provide a codec to GoRouter to '
423+
'prevent extra being dropped during serialization.',
424+
level: Level.WARNING);
425+
}
426+
encodedExtra = <String, Object?>{
427+
RouteMatchListCodec._codecKey: RouteMatchListCodec._jsonCodecName,
428+
RouteMatchListCodec._encodedKey: jsonEncodedExtra,
429+
};
430+
}
431+
403432
return <Object?, Object?>{
404433
RouteMatchListCodec._locationKey: location,
405-
if (encodedExtra != null) RouteMatchListCodec._extraKey: encodedExtra,
434+
RouteMatchListCodec._extraKey: encodedExtra,
406435
if (imperativeMatches != null)
407436
RouteMatchListCodec._imperativeMatchesKey: imperativeMatches,
408437
if (pageKey != null) RouteMatchListCodec._pageKey: pageKey,
@@ -420,13 +449,17 @@ class _RouteMatchListDecoder
420449
RouteMatchList convert(Map<Object?, Object?> input) {
421450
final String rootLocation =
422451
input[RouteMatchListCodec._locationKey]! as String;
423-
final String? encodedExtra =
424-
input[RouteMatchListCodec._extraKey] as String?;
452+
final Map<Object?, Object?> encodedExtra =
453+
input[RouteMatchListCodec._extraKey]! as Map<Object?, Object?>;
425454
final Object? extra;
426-
if (encodedExtra != null) {
427-
extra = json.decoder.convert(encodedExtra);
455+
456+
if (encodedExtra[RouteMatchListCodec._codecKey] ==
457+
RouteMatchListCodec._jsonCodecName) {
458+
extra = json.decoder
459+
.convert(encodedExtra[RouteMatchListCodec._encodedKey]! as String);
428460
} else {
429-
extra = null;
461+
extra = configuration.extraCodec
462+
?.decode(encodedExtra[RouteMatchListCodec._encodedKey]);
430463
}
431464
RouteMatchList matchList =
432465
configuration.findMatch(rootLocation, extra: extra);

0 commit comments

Comments
 (0)