diff --git a/package/lib/src/beam_state.dart b/package/lib/src/beam_state.dart index af33ea7..a1371dd 100644 --- a/package/lib/src/beam_state.dart +++ b/package/lib/src/beam_state.dart @@ -196,7 +196,7 @@ class BeamState with RouteInformationSerializable { ); @override - int get hashCode => hashValues(uri, json.encode(routeState)); + int get hashCode => Object.hash(uri, json.encode(routeState)); @override bool operator ==(Object other) { diff --git a/package/lib/src/beamer.dart b/package/lib/src/beamer.dart index de55902..67ecd38 100644 --- a/package/lib/src/beamer.dart +++ b/package/lib/src/beamer.dart @@ -1,4 +1,5 @@ import 'package:beamer/beamer.dart'; +import 'package:beamer/src/utils.dart'; import 'package:flutter/widgets.dart'; import 'package:beamer/src/path_url_strategy_nonweb.dart' @@ -153,6 +154,7 @@ extension BeamerExtensions on BuildContext { String uri, { Object? routeState, Object? data, + Map? queryParameters, String? popToNamed, TransitionDelegate? transitionDelegate, bool beamBackOnPop = false, @@ -161,7 +163,7 @@ extension BeamerExtensions on BuildContext { bool replaceRouteInformation = false, }) { Beamer.of(this).beamToNamed( - uri, + constructUri(uri, queryParameters), routeState: routeState, data: data, popToNamed: popToNamed, @@ -178,6 +180,7 @@ extension BeamerExtensions on BuildContext { String uri, { Object? routeState, Object? data, + Map? queryParameters, String? popToNamed, TransitionDelegate? transitionDelegate, bool beamBackOnPop = false, @@ -185,7 +188,7 @@ extension BeamerExtensions on BuildContext { bool stacked = true, }) { Beamer.of(this).beamToReplacementNamed( - uri, + constructUri(uri, queryParameters), routeState: routeState, data: data, popToNamed: popToNamed, @@ -201,6 +204,7 @@ extension BeamerExtensions on BuildContext { String uri, { Object? routeState, Object? data, + Map? queryParameters, String? popToNamed, bool beamBackOnPop = false, bool popBeamLocationOnPop = false, @@ -208,7 +212,7 @@ extension BeamerExtensions on BuildContext { bool replaceRouteInformation = false, }) { Beamer.of(this).popToNamed( - uri, + constructUri(uri, queryParameters), routeState: routeState, data: data, popToNamed: popToNamed, diff --git a/package/lib/src/beamer_delegate.dart b/package/lib/src/beamer_delegate.dart index 5c13d27..8d4804b 100644 --- a/package/lib/src/beamer_delegate.dart +++ b/package/lib/src/beamer_delegate.dart @@ -546,9 +546,13 @@ class BeamerDelegate extends RouterDelegate bool popBeamLocationOnPop = false, bool stacked = true, bool replaceRouteInformation = false, + Map? queryParameters, }) { update( - configuration: RouteInformation(location: uri, state: routeState), + configuration: RouteInformation( + location: constructUri(uri, queryParameters), + state: routeState, + ), beamParameters: _currentBeamParameters.copyWith( popConfiguration: popToNamed != null ? RouteInformation(location: popToNamed) : null, @@ -573,10 +577,11 @@ class BeamerDelegate extends RouterDelegate bool beamBackOnPop = false, bool popBeamLocationOnPop = false, bool stacked = true, + Map? queryParameters, }) { removeLastHistoryElement(); beamToNamed( - uri, + constructUri(uri, queryParameters), routeState: routeState, data: data, popToNamed: popToNamed, @@ -604,10 +609,12 @@ class BeamerDelegate extends RouterDelegate bool popBeamLocationOnPop = false, bool stacked = true, bool replaceRouteInformation = false, + Map? queryParameters, }) { + final fullUri = constructUri(uri, queryParameters); while (beamingHistory.isNotEmpty) { final index = beamingHistory.last.history.lastIndexWhere( - (element) => element.routeInformation.location == uri, + (element) => element.routeInformation.location == fullUri, ); if (index == -1) { _disposeBeamLocation(beamingHistory.last); @@ -620,7 +627,7 @@ class BeamerDelegate extends RouterDelegate } } beamToNamed( - uri, + fullUri, routeState: routeState, data: data, popToNamed: popToNamed, diff --git a/package/lib/src/utils.dart b/package/lib/src/utils.dart index b40c7cd..6a50bc9 100644 --- a/package/lib/src/utils.dart +++ b/package/lib/src/utils.dart @@ -309,3 +309,21 @@ extension BeamerRouteInformationExtension on RouteInformation { return location == other.location && state == other.state; } } + +/// Returns the provided `String uri` with the constructed `Map? queryParameters` +/// +/// If `Map? queryParameters` is provided, `String uri` should not containt query parameters. +/// +/// When `queryParameters` is used the query is built from the provided map. +/// Each key and value in the map is percent-encoded and joined using equal and ampersand characters. +/// A value in the map must be either a `String`, or an `Iterable`, where the latter corresponds to multiple values for the same key. +String constructUri(String uri, Map? queryParameters) { + final _inputQueryParameters = Uri.parse(uri).queryParameters; + assert(_inputQueryParameters.isEmpty || (queryParameters?.isEmpty ?? true), + 'Avoid passing an uri that already contains query Parameters and a non-empty `queryParameters`'); + + if (queryParameters?.isEmpty ?? true) return uri; + if (_inputQueryParameters.isNotEmpty) return uri; + + return uri + '?' + Uri(queryParameters: queryParameters).query; +} diff --git a/package/test/beamer_test.dart b/package/test/beamer_test.dart index 4d2d06d..3069264 100644 --- a/package/test/beamer_test.dart +++ b/package/test/beamer_test.dart @@ -36,6 +36,7 @@ void main() { key: beamerKey, routerDelegate: BeamerProvider.of(context)!.routerDelegate, ), + '/test3': (context, state, data) => Container(), }, ), ), @@ -170,4 +171,20 @@ void main() { await tester.pumpWidget(app); expect(testContext!.beamingHistory.length, greaterThan(0)); }); + + testWidgets('Beaming updates location state with queryParameters', + (tester) async { + const queryParams = {'testParam': 'test'}; + await tester.pumpWidget(app); + testContext!.beamToReplacementNamed('/test3', queryParameters: queryParams); + await tester.pump(); + expect(testContext!.currentBeamLocation.state.routeInformation.location, + '/test3?testParam=test'); + expect( + (testContext!.currentBeamLocation.state as BeamState) + .uriBlueprint + .toString(), + '/test3?testParam=test'); + expect(testContext!.currentBeamPages.length, 2); + }); } diff --git a/package/test/path_url_query_parameters_test.dart b/package/test/path_url_query_parameters_test.dart new file mode 100644 index 0000000..1719ba9 --- /dev/null +++ b/package/test/path_url_query_parameters_test.dart @@ -0,0 +1,27 @@ +import 'package:beamer/src/utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test( + 'Only URI was passed', + () { + expect(constructUri('/test', null), '/test'); + }, + ); + + test( + 'URI without queryParameters and queryParameters Map were passed', + () { + expect(constructUri('/test', {'test': 'true'}), '/test?test=true'); + }, + ); + + test( + 'URI wit queryParameters and queryParameters Map were passed', + () { + expect(() { + constructUri('/test?test=true', {'test': 'true'}); + }, throwsAssertionError); + }, + ); +}