From 1d62ff80cd340f403ebf350ff7e1b1f61ed91324 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Tue, 31 Oct 2023 16:03:30 -0700 Subject: [PATCH 1/5] [go_router] Adds an ability to add a custom codec for serializing/deserializing extra --- packages/go_router/CHANGELOG.md | 4 + packages/go_router/doc/navigation.md | 20 +++ packages/go_router/example/README.md | 5 + .../ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../go_router/example/lib/extra_codec.dart | 138 ++++++++++++++++++ .../example/test/extra_codec_test.dart | 23 +++ packages/go_router/lib/src/configuration.dart | 7 + .../lib/src/information_provider.dart | 2 + packages/go_router/lib/src/logging.dart | 4 +- packages/go_router/lib/src/match.dart | 65 +++++++-- packages/go_router/lib/src/router.dart | 5 + packages/go_router/pubspec.yaml | 2 +- packages/go_router/test/extra_codec_test.dart | 111 ++++++++++++++ packages/go_router/test/test_helpers.dart | 28 ++++ 15 files changed, 398 insertions(+), 20 deletions(-) create mode 100644 packages/go_router/example/lib/extra_codec.dart create mode 100644 packages/go_router/example/test/extra_codec_test.dart create mode 100644 packages/go_router/test/extra_codec_test.dart diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 6791a2c917e..63e9cdbf4ba 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 12.1.0 + +- Adds an ability to add a custom codec for serializing/deserializing extra. + ## 12.0.3 - Fixes crashes when dynamically updates routing tables with named routes. diff --git a/packages/go_router/doc/navigation.md b/packages/go_router/doc/navigation.md index f5fa16ac4ba..26f91f38d84 100644 --- a/packages/go_router/doc/navigation.md +++ b/packages/go_router/doc/navigation.md @@ -84,5 +84,25 @@ Returning a value: onTap: () => context.pop(true) ``` +## Using extra +You can provide additional data along with navigation. + +```dart +context.go('/123, extra: 'abc'); +``` + +and retrieve the data from GoRouterState + +```dart +final String extraString = GoRouterState.of(context).extra! as String; +``` + +The extra will go through serialization when storing into the Router. +If you plan to use complex data as extra, consider also provide a codec +to GoRouter so that it won't get dropped during serialization. + +For an example on how to write use complex extra with codec, see +[extra_codec.dart](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart). + [Named routes]: https://pub.dev/documentation/go_router/latest/topics/Named%20routes-topic.html diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index 55f75bca6a6..4208354dbd9 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -41,6 +41,11 @@ An example to demonstrate how to use a `StatefulShellRoute` to create stateful n An example to demonstrate how to handle exception in go_router. +## [Extra Codec](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart) +`flutter run lib/extra_codec.dart` + +An example to demonstrate how use complex object as extra. + ## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books) `flutter run lib/books/main.dart` diff --git a/packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj b/packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj index 0841413a1fd..8f3ef0d66bf 100644 --- a/packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj @@ -156,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3db53b6e1fb..b52b2e698b7 100644 --- a/packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + ), + ], + extraCodec: const MyExtraCodec(), +); + +/// 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 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: [ + const Text( + 'If running in web, use browser backward and forward button to test extra codec after setting extra several times'), + Text('extra for this page is: ${GoRouterState.of(context).extra}'), + ElevatedButton( + onPressed: () => context.go('/', extra: ComplexData1('data')), + child: const Text('Set extra to ComplexData1'), + ), + ElevatedButton( + onPressed: () => context.go('/', extra: ComplexData2('data')), + child: const Text('Set extra to ComplexData2'), + ), + ], + ), + ), + ); + } +} + +/// A complex class. +class ComplexData1 { + /// Create a complex object. + ComplexData1(this.data); + + /// The data. + final String data; + + @override + String toString() => 'ComplexData1(data: $data)'; +} + +/// A complex class. +class ComplexData2 { + /// Create a complex object. + ComplexData2(this.data); + + /// The data. + final String data; + + @override + String toString() => 'ComplexData2(data: $data)'; +} + +/// A codec that can serialize both [ComplexData1] and [ComplexData2]. +class MyExtraCodec extends Codec { + /// Create a codec + const MyExtraCodec(); + @override + Converter get decoder => const _MyExtraDecoder(); + + @override + Converter get encoder => const _MyExtraEncoder(); +} + +class _MyExtraDecoder extends Converter { + const _MyExtraDecoder(); + @override + Object? convert(Object? input) { + if (input == null) { + return null; + } + final List inputAsList = input as List; + if (inputAsList[0] == 'ComplexData1') { + return ComplexData1(inputAsList[1]! as String); + } + if (inputAsList[0] == 'ComplexData2') { + return ComplexData2(inputAsList[1]! as String); + } + throw UnimplementedError(); + } +} + +class _MyExtraEncoder extends Converter { + const _MyExtraEncoder(); + @override + Object? convert(Object? input) { + if (input == null) { + return null; + } + switch (input.runtimeType) { + case ComplexData1: + return ['ComplexData1', (input as ComplexData1).data]; + case ComplexData2: + return ['ComplexData2', (input as ComplexData2).data]; + default: + throw UnimplementedError(); + } + } +} diff --git a/packages/go_router/example/test/extra_codec_test.dart b/packages/go_router/example/test/extra_codec_test.dart new file mode 100644 index 00000000000..989fcbcbec2 --- /dev/null +++ b/packages/go_router/example/test/extra_codec_test.dart @@ -0,0 +1,23 @@ +// 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/extra_codec.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + expect(find.text('extra for this page is: null'), findsOneWidget); + + await tester.tap(find.text('Set extra to ComplexData1')); + await tester.pumpAndSettle(); + expect(find.text('extra for this page is: ComplexData1(data: data)'), + findsOneWidget); + + await tester.tap(find.text('Set extra to ComplexData2')); + await tester.pumpAndSettle(); + expect(find.text('extra for this page is: ComplexData2(data: data)'), + findsOneWidget); + }); +} diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index a5ff0fda5b4..5029aab188d 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -25,6 +26,7 @@ class RouteConfiguration { RouteConfiguration( this._routingConfig, { required this.navigatorKey, + this.extraCodec, }) { _onRoutingTableChanged(); _routingConfig.addListener(_onRoutingTableChanged); @@ -232,6 +234,11 @@ class RouteConfiguration { /// The global key for top level navigator. final GlobalKey navigatorKey; + /// The codec used to encode and decode extra into a serializable format. + /// + /// This is needed if the extra used in GoRouter contains conplex data. + final Codec? extraCodec; + final Map _nameToPath = {}; /// Looks up the url location by a [GoRoute]'s name. diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index 9346c2a20b0..626fe66c149 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -114,6 +114,8 @@ class GoRouteInformationProvider extends RouteInformationProvider .equals(_valueInEngine.state, routeInformation.state)) { return; } + print( + '_valueInEngine.state ${_valueInEngine.state}\n routeInformation.state ${routeInformation.state}'); replace = _valueInEngine == _kEmptyRouteInformation; break; case RouteInformationReportingType.neglect: diff --git a/packages/go_router/lib/src/logging.dart b/packages/go_router/lib/src/logging.dart index 3f39e4dc804..7f0a8ce5a7a 100644 --- a/packages/go_router/lib/src/logging.dart +++ b/packages/go_router/lib/src/logging.dart @@ -16,9 +16,9 @@ final Logger logger = Logger('GoRouter'); bool _enabled = false; /// Logs the message if logging is enabled. -void log(String message) { +void log(String message, {Level level = Level.INFO}) { if (_enabled) { - logger.info(message); + logger.log(level, message); } } diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index e5ffdec31d7..57b9954ef1c 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -2,15 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// This file deal with json. +// ignore_for_file: avoid_dynamic_calls + import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'configuration.dart'; +import 'logging.dart'; import 'misc/errors.dart'; import 'path_utils.dart'; import 'route.dart'; @@ -363,16 +368,20 @@ class RouteMatchList { class RouteMatchListCodec extends Codec> { /// Creates a new [RouteMatchListCodec] object. RouteMatchListCodec(RouteConfiguration configuration) - : decoder = _RouteMatchListDecoder(configuration); + : decoder = _RouteMatchListDecoder(configuration), + encoder = _RouteMatchListEncoder(configuration); static const String _locationKey = 'location'; static const String _extraKey = 'state'; static const String _imperativeMatchesKey = 'imperativeMatches'; static const String _pageKey = 'pageKey'; + static const String _codecKey = 'codec'; + static const String _jsonCodecName = 'json'; + static const String _customCodecName = 'custom'; + static const String _encodedKey = 'encoded'; @override - final Converter> encoder = - const _RouteMatchListEncoder(); + final Converter> encoder; @override final Converter, RouteMatchList> decoder; @@ -380,7 +389,9 @@ class RouteMatchListCodec extends Codec> { class _RouteMatchListEncoder extends Converter> { - const _RouteMatchListEncoder(); + const _RouteMatchListEncoder(this.configuration); + + final RouteConfiguration configuration; @override Map convert(RouteMatchList input) { final List> imperativeMatches = input.matches @@ -394,15 +405,36 @@ class _RouteMatchListEncoder imperativeMatches: imperativeMatches); } - static Map _toPrimitives(String location, Object? extra, + Map _toPrimitives(String location, Object? extra, {List>? imperativeMatches, String? pageKey}) { - String? encodedExtra; - try { - encodedExtra = json.encoder.convert(extra); - } on JsonUnsupportedObjectError {/* give up if not serializable */} + Map encodedExtra; + if (configuration.extraCodec != null) { + encodedExtra = { + RouteMatchListCodec._codecKey: RouteMatchListCodec._customCodecName, + RouteMatchListCodec._encodedKey: + configuration.extraCodec?.encode(extra), + }; + } else { + String jsonEncodedExtra; + try { + jsonEncodedExtra = json.encoder.convert(extra); + } on JsonUnsupportedObjectError { + jsonEncodedExtra = json.encoder.convert(null); + log( + 'An extra with complex data type ${extra.runtimeType} is provided ' + 'without an codec. Consider provide an extraCodec to GoRouter to ' + 'prevent extra being dropped during serialization.', + level: Level.WARNING); + } + encodedExtra = { + RouteMatchListCodec._codecKey: RouteMatchListCodec._jsonCodecName, + RouteMatchListCodec._encodedKey: jsonEncodedExtra, + }; + } + return { RouteMatchListCodec._locationKey: location, - if (encodedExtra != null) RouteMatchListCodec._extraKey: encodedExtra, + RouteMatchListCodec._extraKey: encodedExtra, if (imperativeMatches != null) RouteMatchListCodec._imperativeMatchesKey: imperativeMatches, if (pageKey != null) RouteMatchListCodec._pageKey: pageKey, @@ -420,13 +452,16 @@ class _RouteMatchListDecoder RouteMatchList convert(Map input) { final String rootLocation = input[RouteMatchListCodec._locationKey]! as String; - final String? encodedExtra = - input[RouteMatchListCodec._extraKey] as String?; + final dynamic encodedExtra = input[RouteMatchListCodec._extraKey]; final Object? extra; - if (encodedExtra != null) { - extra = json.decoder.convert(encodedExtra); + + if (encodedExtra[RouteMatchListCodec._codecKey] == + RouteMatchListCodec._jsonCodecName) { + extra = json.decoder + .convert(encodedExtra[RouteMatchListCodec._encodedKey]! as String); } else { - extra = null; + extra = configuration.extraCodec + ?.decode(encodedExtra[RouteMatchListCodec._encodedKey]); } RouteMatchList matchList = configuration.findMatch(rootLocation, extra: extra); diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index f7f8e2b0f3a..0f27dcf48c4 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -121,6 +122,7 @@ class GoRouter implements RouterConfig { /// The `routes` must not be null and must contain an [GoRouter] to match `/`. factory GoRouter({ required List routes, + Codec? extraCodec, GoExceptionHandler? onException, GoRouterPageBuilder? errorPageBuilder, GoRouterWidgetBuilder? errorBuilder, @@ -144,6 +146,7 @@ class GoRouter implements RouterConfig { redirect: redirect ?? RoutingConfig._defaultRedirect, redirectLimit: redirectLimit), ), + extraCodec: extraCodec, onException: onException, errorPageBuilder: errorPageBuilder, errorBuilder: errorBuilder, @@ -165,6 +168,7 @@ class GoRouter implements RouterConfig { /// See [routing_config.dart](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/routing_config.dart). GoRouter.routingConfig({ required ValueListenable routingConfig, + Codec? extraCodec, GoExceptionHandler? onException, GoRouterPageBuilder? errorPageBuilder, GoRouterWidgetBuilder? errorBuilder, @@ -201,6 +205,7 @@ class GoRouter implements RouterConfig { configuration = RouteConfiguration( _routingConfig, navigatorKey: navigatorKey, + extraCodec: extraCodec, ); final ParserExceptionHandler? parserExceptionHandler; diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index e2c5cb5aa0b..f37ba325b30 100644 --- a/packages/go_router/pubspec.yaml +++ b/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: 12.0.3 +version: 12.1.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 diff --git a/packages/go_router/test/extra_codec_test.dart b/packages/go_router/test/extra_codec_test.dart new file mode 100644 index 00000000000..3c858cad17d --- /dev/null +++ b/packages/go_router/test/extra_codec_test.dart @@ -0,0 +1,111 @@ +// 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 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; + +import 'test_helpers.dart'; + +void main() { + testWidgets('router rebuild with extra codec works', + (WidgetTester tester) async { + const String initialString = 'some string'; + const String empty = 'empty'; + final GoRouter router = GoRouter( + initialLocation: '/', + extraCodec: ComplexDataCodec(), + initialExtra: ComplexData(initialString), + routes: [ + GoRoute( + path: '/', + builder: (_, GoRouterState state) { + return Text((state.extra as ComplexData?)?.data ?? empty); + }), + ], + redirect: (BuildContext context, _) { + // Set up dependency. + SimpleDependencyProvider.of(context); + return null; + }, + ); + final SimpleDependency dependency = SimpleDependency(); + addTearDown(() => dependency.dispose()); + + await tester.pumpWidget( + SimpleDependencyProvider( + dependency: dependency, + child: MaterialApp.router( + routerConfig: router, + ), + ), + ); + expect(find.text(initialString), findsOneWidget); + dependency.boolProperty = !dependency.boolProperty; + + await tester.pumpAndSettle(); + expect(find.text(initialString), findsOneWidget); + }); + + testWidgets('Restores state correctly', (WidgetTester tester) async { + const String initialString = 'some string'; + const String empty = 'empty'; + final List routes = [ + GoRoute( + path: '/', + builder: (_, GoRouterState state) { + return Text((state.extra as ComplexData?)?.data ?? empty); + }, + ), + ]; + + await createRouter( + routes, + tester, + initialExtra: ComplexData(initialString), + restorationScopeId: 'test', + extraCodec: ComplexDataCodec(), + ); + expect(find.text(initialString), findsOneWidget); + + await tester.restartAndRestore(); + + await tester.pumpAndSettle(); + expect(find.text(initialString), findsOneWidget); + }); +} + +class ComplexData { + ComplexData(this.data); + final String data; +} + +class ComplexDataCodec extends Codec { + @override + Converter get decoder => ComplexDataDecoder(); + @override + Converter get encoder => ComplexDataEncoder(); +} + +class ComplexDataDecoder extends Converter { + @override + ComplexData? convert(Object? input) { + if (input == null) { + return null; + } + return ComplexData(input as String); + } +} + +class ComplexDataEncoder extends Converter { + @override + Object? convert(ComplexData? input) { + if (input == null) { + return null; + } + return input.data; + } +} diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 76ec2874a7a..3db0b457908 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -4,6 +4,8 @@ // ignore_for_file: cascade_invocations, diagnostic_describe_all_properties +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -167,6 +169,7 @@ Future createRouter( GlobalKey? navigatorKey, GoRouterWidgetBuilder? errorBuilder, String? restorationScopeId, + Codec? extraCodec, GoExceptionHandler? onException, bool requestFocus = true, bool overridePlatformDefaultLocation = false, @@ -174,6 +177,7 @@ Future createRouter( final GoRouter goRouter = GoRouter( routes: routes, redirect: redirect, + extraCodec: extraCodec, initialLocation: initialLocation, onException: onException, initialExtra: initialExtra, @@ -391,3 +395,27 @@ RouteConfiguration createRouteConfiguration({ )), navigatorKey: navigatorKey); } + +class SimpleDependencyProvider extends InheritedNotifier { + const SimpleDependencyProvider( + {super.key, required SimpleDependency dependency, required super.child}) + : super(notifier: dependency); + + static SimpleDependency of(BuildContext context) { + final SimpleDependencyProvider result = + context.dependOnInheritedWidgetOfExactType()!; + return result.notifier!; + } +} + +class SimpleDependency extends ChangeNotifier { + bool get boolProperty => _boolProperty; + bool _boolProperty = true; + set boolProperty(bool value) { + if (value == _boolProperty) { + return; + } + _boolProperty = value; + notifyListeners(); + } +} From 7f008204150099b61074833cede8808a49f41f69 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Tue, 31 Oct 2023 16:20:46 -0700 Subject: [PATCH 2/5] update --- packages/go_router/doc/navigation.md | 2 +- packages/go_router/lib/src/information_provider.dart | 2 -- packages/go_router/lib/src/match.dart | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/go_router/doc/navigation.md b/packages/go_router/doc/navigation.md index 26f91f38d84..f6fa574a6fe 100644 --- a/packages/go_router/doc/navigation.md +++ b/packages/go_router/doc/navigation.md @@ -97,7 +97,7 @@ and retrieve the data from GoRouterState final String extraString = GoRouterState.of(context).extra! as String; ``` -The extra will go through serialization when storing into the Router. +The extra will go through serialization when it is stored in browser. If you plan to use complex data as extra, consider also provide a codec to GoRouter so that it won't get dropped during serialization. diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index 626fe66c149..9346c2a20b0 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -114,8 +114,6 @@ class GoRouteInformationProvider extends RouteInformationProvider .equals(_valueInEngine.state, routeInformation.state)) { return; } - print( - '_valueInEngine.state ${_valueInEngine.state}\n routeInformation.state ${routeInformation.state}'); replace = _valueInEngine == _kEmptyRouteInformation; break; case RouteInformationReportingType.neglect: diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index 57b9954ef1c..e9c840cc544 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// This file deal with json. +// This file deals with json. // ignore_for_file: avoid_dynamic_calls import 'dart:async'; @@ -422,7 +422,7 @@ class _RouteMatchListEncoder jsonEncodedExtra = json.encoder.convert(null); log( 'An extra with complex data type ${extra.runtimeType} is provided ' - 'without an codec. Consider provide an extraCodec to GoRouter to ' + 'without a codec. Consider provide a codec to GoRouter to ' 'prevent extra being dropped during serialization.', level: Level.WARNING); } From ce35b85f045790538b3e2786a1bbf255195d6149 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Fri, 3 Nov 2023 12:24:01 -0700 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Michael Goderbauer --- packages/go_router/doc/navigation.md | 6 +++--- packages/go_router/example/README.md | 2 +- packages/go_router/example/lib/extra_codec.dart | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/go_router/doc/navigation.md b/packages/go_router/doc/navigation.md index f6fa574a6fe..f351d2b821d 100644 --- a/packages/go_router/doc/navigation.md +++ b/packages/go_router/doc/navigation.md @@ -97,11 +97,11 @@ and retrieve the data from GoRouterState final String extraString = GoRouterState.of(context).extra! as String; ``` -The extra will go through serialization when it is stored in browser. -If you plan to use complex data as extra, consider also provide a codec +The extra data will go through serialization when it is stored in the browser. +If you plan to use complex data as extra, consider also providing a codec to GoRouter so that it won't get dropped during serialization. -For an example on how to write use complex extra with codec, see +For an example on how to use complex data in extra with a codec, see [extra_codec.dart](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart). diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md index 4208354dbd9..cac540fa0f8 100644 --- a/packages/go_router/example/README.md +++ b/packages/go_router/example/README.md @@ -44,7 +44,7 @@ An example to demonstrate how to handle exception in go_router. ## [Extra Codec](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart) `flutter run lib/extra_codec.dart` -An example to demonstrate how use complex object as extra. +An example to demonstrate how to use a complex object as extra. ## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books) `flutter run lib/books/main.dart` diff --git a/packages/go_router/example/lib/extra_codec.dart b/packages/go_router/example/lib/extra_codec.dart index 19e1fde2551..5c3236fe4c3 100644 --- a/packages/go_router/example/lib/extra_codec.dart +++ b/packages/go_router/example/lib/extra_codec.dart @@ -7,10 +7,10 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -/// This sample app demonstrates how to provide an extra codec for complex extra. +/// This sample app demonstrates how to provide a codec for complex extra data. void main() => runApp(const MyApp()); -/// The route configuration. +/// The router configuration. final GoRouter _router = GoRouter( routes: [ GoRoute( @@ -35,9 +35,9 @@ class MyApp extends StatelessWidget { } } -/// The home screen +/// The home screen. class HomeScreen extends StatelessWidget { - /// Constructs a [HomeScreen] + /// Constructs a [HomeScreen]. const HomeScreen({super.key}); @override @@ -49,8 +49,8 @@ class HomeScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( - 'If running in web, use browser backward and forward button to test extra codec after setting extra several times'), - Text('extra for this page is: ${GoRouterState.of(context).extra}'), + 'If running in web, use the browser's backward and forward button to test extra codec after setting extra several times.'), + Text('The extra for this page is: ${GoRouterState.of(context).extra}'), ElevatedButton( onPressed: () => context.go('/', extra: ComplexData1('data')), child: const Text('Set extra to ComplexData1'), @@ -92,7 +92,7 @@ class ComplexData2 { /// A codec that can serialize both [ComplexData1] and [ComplexData2]. class MyExtraCodec extends Codec { - /// Create a codec + /// Create a codec. const MyExtraCodec(); @override Converter get decoder => const _MyExtraDecoder(); From e0f0eae1a08f87234bfad0f7da3adce4f61abc46 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 3 Nov 2023 12:50:23 -0700 Subject: [PATCH 4/5] update --- packages/go_router/example/lib/extra_codec.dart | 9 +++++---- packages/go_router/lib/src/configuration.dart | 10 +++++++++- packages/go_router/lib/src/match.dart | 8 +++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/go_router/example/lib/extra_codec.dart b/packages/go_router/example/lib/extra_codec.dart index 5c3236fe4c3..497312cb530 100644 --- a/packages/go_router/example/lib/extra_codec.dart +++ b/packages/go_router/example/lib/extra_codec.dart @@ -49,8 +49,9 @@ class HomeScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( - 'If running in web, use the browser's backward and forward button to test extra codec after setting extra several times.'), - Text('The extra for this page is: ${GoRouterState.of(context).extra}'), + "If running in web, use the browser's backward and forward button to test extra codec after setting extra several times."), + Text( + 'The extra for this page is: ${GoRouterState.of(context).extra}'), ElevatedButton( onPressed: () => context.go('/', extra: ComplexData1('data')), child: const Text('Set extra to ComplexData1'), @@ -115,7 +116,7 @@ class _MyExtraDecoder extends Converter { if (inputAsList[0] == 'ComplexData2') { return ComplexData2(inputAsList[1]! as String); } - throw UnimplementedError(); + throw FormatException('Unable tp parse input: $input'); } } @@ -132,7 +133,7 @@ class _MyExtraEncoder extends Converter { case ComplexData2: return ['ComplexData2', (input as ComplexData2).data]; default: - throw UnimplementedError(); + throw FormatException('Cannot encode type ${input.runtimeType}'); } } } diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 5029aab188d..5149d18e852 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -236,7 +236,15 @@ class RouteConfiguration { /// The codec used to encode and decode extra into a serializable format. /// - /// This is needed if the extra used in GoRouter contains conplex data. + /// When navigating using [GoRouter.go] or [GoRouter.push], one can provide + /// an `extra` parameter along with it. If the extra contains complex data, + /// consider provide a codec for serializing and deserializing the extra data. + /// + /// See also: + /// * [Navigation](https://pub.dev/documentation/go_router/latest/topics/Navigation-topic.html) + /// topic. + /// * [extra_codec](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart) + /// example. final Codec? extraCodec; final Map _nameToPath = {}; diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index e9c840cc544..3b7a9468869 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// This file deals with json. -// ignore_for_file: avoid_dynamic_calls - import 'dart:async'; import 'dart:convert'; @@ -363,7 +360,7 @@ class RouteMatchList { /// Handles encoding and decoding of [RouteMatchList] objects to a format /// suitable for using with [StandardMessageCodec]. /// -/// The primary use of this class is for state restoration. +/// The primary use of this class is for state restoration and browser history. @internal class RouteMatchListCodec extends Codec> { /// Creates a new [RouteMatchListCodec] object. @@ -452,7 +449,8 @@ class _RouteMatchListDecoder RouteMatchList convert(Map input) { final String rootLocation = input[RouteMatchListCodec._locationKey]! as String; - final dynamic encodedExtra = input[RouteMatchListCodec._extraKey]; + final Map encodedExtra = + input[RouteMatchListCodec._extraKey]! as Map; final Object? extra; if (encodedExtra[RouteMatchListCodec._codecKey] == From 39412b5ebe94cebd516109744dd077e53883e771 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 3 Nov 2023 12:58:33 -0700 Subject: [PATCH 5/5] update --- packages/go_router/example/test/extra_codec_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/go_router/example/test/extra_codec_test.dart b/packages/go_router/example/test/extra_codec_test.dart index 989fcbcbec2..7358cfc614e 100644 --- a/packages/go_router/example/test/extra_codec_test.dart +++ b/packages/go_router/example/test/extra_codec_test.dart @@ -8,16 +8,16 @@ import 'package:go_router_examples/extra_codec.dart' as example; void main() { testWidgets('example works', (WidgetTester tester) async { await tester.pumpWidget(const example.MyApp()); - expect(find.text('extra for this page is: null'), findsOneWidget); + expect(find.text('The extra for this page is: null'), findsOneWidget); await tester.tap(find.text('Set extra to ComplexData1')); await tester.pumpAndSettle(); - expect(find.text('extra for this page is: ComplexData1(data: data)'), + expect(find.text('The extra for this page is: ComplexData1(data: data)'), findsOneWidget); await tester.tap(find.text('Set extra to ComplexData2')); await tester.pumpAndSettle(); - expect(find.text('extra for this page is: ComplexData2(data: data)'), + expect(find.text('The extra for this page is: ComplexData2(data: data)'), findsOneWidget); }); }