Unverified Commit c38feb3b authored by chunhtai's avatar chunhtai Committed by GitHub

Router replaces browser history entry if state changes (#83509)

parent 28f311ad
...@@ -67,20 +67,34 @@ class SystemNavigator { ...@@ -67,20 +67,34 @@ class SystemNavigator {
/// Notifies the platform for a route information change. /// Notifies the platform for a route information change.
/// ///
/// On web, creates a new browser history entry and update URL with the route /// On web, this method behaves differently based on the single-entry or
/// information. Whether the history holds one entry or multiple entries is /// multiple-entries history mode. Use the [selectSingleEntryHistory] and
/// determined by [selectSingleEntryHistory] and [selectMultiEntryHistory]. /// [selectMultiEntryHistory] to toggle between modes.
/// ///
/// Currently, this is ignored on other platforms. /// For single-entry mode, this method replaces the current URL and state in
/// the current history entry. The flag `replace` is ignored.
///
/// For multiple-entries mode, this method creates a new history entry on top
/// of the current entry if the `replace` is false, thus the user will
/// be on a new history entry as if the user has visited a new page, and the
/// browser back button brings the user back to the previous entry. If
/// `replace` is true, this method only updates the URL and the state in the
/// current history entry without pushing a new one.
///
/// This method is ignored on other platforms.
///
/// The `replace` flag defaults to false.
static Future<void> routeInformationUpdated({ static Future<void> routeInformationUpdated({
required String location, required String location,
Object? state, Object? state,
bool replace = false,
}) { }) {
return SystemChannels.navigation.invokeMethod<void>( return SystemChannels.navigation.invokeMethod<void>(
'routeInformationUpdated', 'routeInformationUpdated',
<String, dynamic>{ <String, dynamic>{
'location': location, 'location': location,
'state': state, 'state': state,
'replace': replace,
}, },
); );
} }
......
...@@ -198,16 +198,28 @@ class RouteInformation { ...@@ -198,16 +198,28 @@ class RouteInformation {
/// retrieve the new route information from the [routerDelegate]'s /// retrieve the new route information from the [routerDelegate]'s
/// [RouterDelegate.currentConfiguration] method and the /// [RouterDelegate.currentConfiguration] method and the
/// [routeInformationParser]'s [RouteInformationParser.restoreRouteInformation] /// [routeInformationParser]'s [RouteInformationParser.restoreRouteInformation]
/// method. If the location in the new route information is different from the /// method.
/// current location, the router sends the new route information to the ///
/// [routeInformationProvider]'s /// If the location in the new route information is different from the
/// [RouteInformationProvider.routerReportsNewRouteInformation] method. That /// current location, this is considered to be a navigation event, the router
/// method as implemented in [PlatformRouteInformationProvider] uses /// sends the new route information to the [routeInformationProvider]'s
/// [RouteInformationProvider.routerReportsNewRouteInformation] method with
/// `isNavigation` equals to true. That method as implemented in
/// [PlatformRouteInformationProvider] uses
/// [SystemNavigator.routeInformationUpdated] to notify the engine, and through /// [SystemNavigator.routeInformationUpdated] to notify the engine, and through
/// that the browser, of the new URL. /// that the browser, to create a history entry with the new url if the
/// `isNavigation` is true.
///
/// If the location is the same as the current location but different state,
/// the router still sends the new route information to the
/// [routeInformationProvider]'s
/// [RouteInformationProvider.routerReportsNewRouteInformation] but with
/// `isNavigation` equals to false. This causes
/// [PlatformRouteInformationProvider] replace current history entry instead
/// of creating a new one.
/// ///
/// One can force the [Router] to report new route information to the /// One can force the [Router] to report new route information as navigation
/// [routeInformationProvider] (and thus the browser) even if the /// event to the [routeInformationProvider] (and thus the browser) even if the
/// [RouteInformation.location] has not changed by calling the [Router.navigate] /// [RouteInformation.location] has not changed by calling the [Router.navigate]
/// method with a callback that performs the state change. This allows one to /// method with a callback that performs the state change. This allows one to
/// support the browser's back and forward buttons without changing the URL. For /// support the browser's back and forward buttons without changing the URL. For
...@@ -218,10 +230,10 @@ class RouteInformation { ...@@ -218,10 +230,10 @@ class RouteInformation {
/// clicks the back button, the app will go back to the previous scroll position /// clicks the back button, the app will go back to the previous scroll position
/// without changing the URL in the location bar. /// without changing the URL in the location bar.
/// ///
/// One can also force the [Router] to ignore application state changes by /// One can also force the [Router] to ignore a navigation event by making
/// making those changes during a callback passed to [Router.neglect]. The /// those changes during a callback passed to [Router.neglect]. The [Router]
/// [Router] will not report any route information even if it detects location /// will not report the route information with `isNavigation` equals to false
/// change as a result of running the callback. /// even if it detects location change as the result of running the callback.
/// ///
/// To opt out of URL updates entirely, pass null for [routeInformationProvider] /// To opt out of URL updates entirely, pass null for [routeInformationProvider]
/// and [routeInformationParser]. This is not recommended in general, but may be /// and [routeInformationParser]. This is not recommended in general, but may be
...@@ -392,8 +404,8 @@ class Router<T> extends StatefulWidget { ...@@ -392,8 +404,8 @@ class Router<T> extends StatefulWidget {
return scope?.routerState.widget as Router<T>?; return scope?.routerState.widget as Router<T>?;
} }
/// Forces the [Router] to run the [callback] and reports the route /// Forces the [Router] to run the [callback] and create a new history
/// information back to the engine. /// entry in the browser.
/// ///
/// The web application relies on the [Router] to report new route information /// The web application relies on the [Router] to report new route information
/// in order to create browser history entry. The [Router] will only report /// in order to create browser history entry. The [Router] will only report
...@@ -414,8 +426,8 @@ class Router<T> extends StatefulWidget { ...@@ -414,8 +426,8 @@ class Router<T> extends StatefulWidget {
/// ///
/// * [Router]: see the "URL updates for web applications" section for more /// * [Router]: see the "URL updates for web applications" section for more
/// information about route information reporting. /// information about route information reporting.
/// * [neglect]: which forces the [Router] to not report the route /// * [neglect]: which forces the [Router] to not create a new history entry
/// information even if location does change. /// even if location does change.
static void navigate(BuildContext context, VoidCallback callback) { static void navigate(BuildContext context, VoidCallback callback) {
final _RouterScope scope = context final _RouterScope scope = context
.getElementForInheritedWidgetOfExactType<_RouterScope>()! .getElementForInheritedWidgetOfExactType<_RouterScope>()!
...@@ -423,24 +435,27 @@ class Router<T> extends StatefulWidget { ...@@ -423,24 +435,27 @@ class Router<T> extends StatefulWidget {
scope.routerState._setStateWithExplicitReportStatus(_IntentionToReportRouteInformation.must, callback); scope.routerState._setStateWithExplicitReportStatus(_IntentionToReportRouteInformation.must, callback);
} }
/// Forces the [Router] to run the [callback] without reporting the route /// Forces the [Router] to run the [callback] without creating a new history
/// information back to the engine. /// entry in the browser.
///
/// Use this method if you don't want the [Router] to report the new route
/// information even if it detects changes as a result of running the
/// [callback].
/// ///
/// The web application relies on the [Router] to report new route information /// The web application relies on the [Router] to report new route information
/// in order to create browser history entry. The [Router] will report them /// in order to create browser history entry. The [Router] will report them
/// automatically if it detects the [RouteInformation.location] changes. You /// automatically if it detects the [RouteInformation.location] changes.
/// can use this method if you want to navigate to a new route without ///
/// creating the browser history entry. /// Creating a new route history entry makes users feel they have visited a
/// new page, and the browser back button brings them back to previous history
/// entry. Use this method if you don't want the [Router] to create a new
/// route information even if it detects changes as a result of running the
/// [callback].
///
/// Using this method will still update the URL and state in current history
/// entry.
/// ///
/// See also: /// See also:
/// ///
/// * [Router]: see the "URL updates for web applications" section for more /// * [Router]: see the "URL updates for web applications" section for more
/// information about route information reporting. /// information about route information reporting.
/// * [navigate]: which forces the [Router] to report the route information /// * [navigate]: which forces the [Router] to create a new history entry
/// even if location does not change. /// even if location does not change.
static void neglect(BuildContext context, VoidCallback callback) { static void neglect(BuildContext context, VoidCallback callback) {
final _RouterScope scope = context final _RouterScope scope = context
...@@ -497,8 +512,6 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin { ...@@ -497,8 +512,6 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin {
bool _routeInformationReportingTaskScheduled = false; bool _routeInformationReportingTaskScheduled = false;
String? _lastSeenLocation;
void _scheduleRouteInformationReportingTask() { void _scheduleRouteInformationReportingTask() {
if (_routeInformationReportingTaskScheduled || widget.routeInformationProvider == null) if (_routeInformationReportingTaskScheduled || widget.routeInformationProvider == null)
return; return;
...@@ -512,23 +525,29 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin { ...@@ -512,23 +525,29 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin {
_routeInformationReportingTaskScheduled = false; _routeInformationReportingTaskScheduled = false;
if (_routeInformation.value != null) { if (_routeInformation.value != null) {
final RouteInformation routeInformation = _routeInformation.value!; final RouteInformation oldRouteInformation = widget.routeInformationProvider!.value;
final RouteInformation currentRouteInformation = _routeInformation.value!;
switch (_currentIntentionToReport) { switch (_currentIntentionToReport) {
case _IntentionToReportRouteInformation.none: case _IntentionToReportRouteInformation.none:
assert(false, '_reportRouteInformation must not be called with _IntentionToReportRouteInformation.none'); assert(false, '_reportRouteInformation must not be called with _IntentionToReportRouteInformation.none');
return; return;
case _IntentionToReportRouteInformation.ignore: case _IntentionToReportRouteInformation.ignore:
if (oldRouteInformation.location != currentRouteInformation.location ||
oldRouteInformation.state != currentRouteInformation.state) {
widget.routeInformationProvider!.routerReportsNewRouteInformation(currentRouteInformation, isNavigation: false);
}
break; break;
case _IntentionToReportRouteInformation.maybe: case _IntentionToReportRouteInformation.maybe:
if (_lastSeenLocation != routeInformation.location) { if (oldRouteInformation.location != currentRouteInformation.location) {
widget.routeInformationProvider!.routerReportsNewRouteInformation(routeInformation); widget.routeInformationProvider!.routerReportsNewRouteInformation(currentRouteInformation);
} else if (oldRouteInformation.state != currentRouteInformation.state) {
widget.routeInformationProvider!.routerReportsNewRouteInformation(currentRouteInformation, isNavigation: false);
} }
break; break;
case _IntentionToReportRouteInformation.must: case _IntentionToReportRouteInformation.must:
widget.routeInformationProvider!.routerReportsNewRouteInformation(routeInformation); widget.routeInformationProvider!.routerReportsNewRouteInformation(currentRouteInformation);
break; break;
} }
_lastSeenLocation = routeInformation.location;
} }
_currentIntentionToReport = _IntentionToReportRouteInformation.none; _currentIntentionToReport = _IntentionToReportRouteInformation.none;
} }
...@@ -621,7 +640,6 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin { ...@@ -621,7 +640,6 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin {
void _processRouteInformation(RouteInformation information, ValueGetter<_DelegateRouteSetter<T>> delegateRouteSetter) { void _processRouteInformation(RouteInformation information, ValueGetter<_DelegateRouteSetter<T>> delegateRouteSetter) {
_currentRouteInformationParserTransaction = Object(); _currentRouteInformationParserTransaction = Object();
_currentRouterDelegateTransaction = Object(); _currentRouterDelegateTransaction = Object();
_lastSeenLocation = information.location;
widget.routeInformationParser! widget.routeInformationParser!
.parseRouteInformation(information) .parseRouteInformation(information)
.then<T>(_verifyRouteInformationParserStillCurrent(_currentRouteInformationParserTransaction, widget)) .then<T>(_verifyRouteInformationParserStillCurrent(_currentRouteInformationParserTransaction, widget))
...@@ -1301,8 +1319,7 @@ abstract class RouterDelegate<T> extends Listenable { ...@@ -1301,8 +1319,7 @@ abstract class RouterDelegate<T> extends Listenable {
/// from the [Router] back to the engine by overriding the /// from the [Router] back to the engine by overriding the
/// [routerReportsNewRouteInformation]. /// [routerReportsNewRouteInformation].
abstract class RouteInformationProvider extends ValueListenable<RouteInformation> { abstract class RouteInformationProvider extends ValueListenable<RouteInformation> {
/// A callback called when the [Router] widget detects any navigation event /// A callback called when the [Router] widget reports new route information
/// due to state changes.
/// ///
/// The subclasses can override this method to update theirs values or trigger /// The subclasses can override this method to update theirs values or trigger
/// other side effects. For example, the [PlatformRouteInformationProvider] /// other side effects. For example, the [PlatformRouteInformationProvider]
...@@ -1310,7 +1327,17 @@ abstract class RouteInformationProvider extends ValueListenable<RouteInformation ...@@ -1310,7 +1327,17 @@ abstract class RouteInformationProvider extends ValueListenable<RouteInformation
/// ///
/// The [routeInformation] is the new route information after the navigation /// The [routeInformation] is the new route information after the navigation
/// event. /// event.
void routerReportsNewRouteInformation(RouteInformation routeInformation) {} ///
/// The [isNavigation] denotes whether the new route information is generated
/// as a result of a navigation event. This information can be useful in a
/// web application, for example, the [PlatformRouteInformationProvider] uses
/// this flag to decide whether to create a browser history entry that enables
/// browser backward and forward buttons.
///
/// For more information on how [Router] determines a navigation event, see
/// the "URL updates for web applications" section in the [Router]
/// documentation.
void routerReportsNewRouteInformation(RouteInformation routeInformation, {bool isNavigation = true}) {}
} }
/// The route information provider that propagates the platform route information changes. /// The route information provider that propagates the platform route information changes.
...@@ -1333,11 +1360,12 @@ class PlatformRouteInformationProvider extends RouteInformationProvider with Wid ...@@ -1333,11 +1360,12 @@ class PlatformRouteInformationProvider extends RouteInformationProvider with Wid
}) : _value = initialRouteInformation; }) : _value = initialRouteInformation;
@override @override
void routerReportsNewRouteInformation(RouteInformation routeInformation) { void routerReportsNewRouteInformation(RouteInformation routeInformation, {bool isNavigation = true}) {
SystemNavigator.selectMultiEntryHistory(); SystemNavigator.selectMultiEntryHistory();
SystemNavigator.routeInformationUpdated( SystemNavigator.routeInformationUpdated(
location: routeInformation.location!, location: routeInformation.location!,
state: routeInformation.state, state: routeInformation.state,
replace: !isNavigation,
); );
_value = routeInformation; _value = routeInformation;
} }
......
...@@ -43,11 +43,15 @@ void main() { ...@@ -43,11 +43,15 @@ void main() {
]); ]);
await verify(() => SystemNavigator.routeInformationUpdated(location: 'a'), <Object>[ await verify(() => SystemNavigator.routeInformationUpdated(location: 'a'), <Object>[
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'location': 'a', 'state': null }), isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'location': 'a', 'state': null, 'replace': false }),
]); ]);
await verify(() => SystemNavigator.routeInformationUpdated(location: 'a', state: true), <Object>[ await verify(() => SystemNavigator.routeInformationUpdated(location: 'a', state: true), <Object>[
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'location': 'a', 'state': true }), isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'location': 'a', 'state': true, 'replace': false }),
]);
await verify(() => SystemNavigator.routeInformationUpdated(location: 'a', state: true, replace: true), <Object>[
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'location': 'a', 'state': true, 'replace': true }),
]); ]);
await verify(() => SystemNavigator.routeUpdated(routeName: 'a', previousRouteName: 'b'), <Object>[ await verify(() => SystemNavigator.routeUpdated(routeName: 'a', previousRouteName: 'b'), <Object>[
......
...@@ -69,6 +69,7 @@ void main() { ...@@ -69,6 +69,7 @@ void main() {
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'location': '/', 'location': '/',
'state': null, 'state': null,
'replace': false,
}, },
), ),
]); ]);
...@@ -86,6 +87,7 @@ void main() { ...@@ -86,6 +87,7 @@ void main() {
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'location': '/A', 'location': '/A',
'state': null, 'state': null,
'replace': false,
}, },
), ),
); );
...@@ -103,6 +105,7 @@ void main() { ...@@ -103,6 +105,7 @@ void main() {
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'location': '/', 'location': '/',
'state': null, 'state': null,
'replace': false,
}, },
), ),
); );
...@@ -174,6 +177,7 @@ void main() { ...@@ -174,6 +177,7 @@ void main() {
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'location': '/', 'location': '/',
'state': null, 'state': null,
'replace': false,
}, },
), ),
]); ]);
...@@ -191,6 +195,7 @@ void main() { ...@@ -191,6 +195,7 @@ void main() {
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'location': '/A', 'location': '/A',
'state': null, 'state': null,
'replace': false,
}, },
), ),
); );
...@@ -208,6 +213,7 @@ void main() { ...@@ -208,6 +213,7 @@ void main() {
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'location': '/B', 'location': '/B',
'state': null, 'state': null,
'replace': false,
}, },
), ),
); );
...@@ -243,6 +249,7 @@ void main() { ...@@ -243,6 +249,7 @@ void main() {
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'location': '/home', 'location': '/home',
'state': null, 'state': null,
'replace': false,
}, },
), ),
]); ]);
...@@ -294,6 +301,7 @@ void main() { ...@@ -294,6 +301,7 @@ void main() {
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
'location': 'update', 'location': 'update',
'state': 'state', 'state': 'state',
'replace': false,
}), }),
]); ]);
}); });
......
...@@ -477,11 +477,14 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester ...@@ -477,11 +477,14 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester
testWidgets('router does report URL change correctly', (WidgetTester tester) async { testWidgets('router does report URL change correctly', (WidgetTester tester) async {
RouteInformation? reportedRouteInformation; RouteInformation? reportedRouteInformation;
bool? reportedIsNavigation;
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider( final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(
onRouterReport: (RouteInformation information) { onRouterReport: (RouteInformation information, bool isNavigation) {
// Makes sure we only report once after manually cleaning up. // Makes sure we only report once after manually cleaning up.
expect(reportedRouteInformation, isNull); expect(reportedRouteInformation, isNull);
expect(reportedIsNavigation, isNull);
reportedRouteInformation = information; reportedRouteInformation = information;
reportedIsNavigation = isNavigation;
}, },
); );
final SimpleRouterDelegate delegate = SimpleRouterDelegate( final SimpleRouterDelegate delegate = SimpleRouterDelegate(
...@@ -519,35 +522,44 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester ...@@ -519,35 +522,44 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester
expect(find.text('initial'), findsNothing); expect(find.text('initial'), findsNothing);
expect(find.text('update'), findsOneWidget); expect(find.text('update'), findsOneWidget);
expect(reportedRouteInformation!.location, 'update'); expect(reportedRouteInformation!.location, 'update');
expect(reportedIsNavigation, isTrue);
// The router should not report if only state changes. // The router should report as non navigation event if only state changes.
reportedRouteInformation = null; reportedRouteInformation = null;
reportedIsNavigation = null;
delegate.routeInformation = const RouteInformation( delegate.routeInformation = const RouteInformation(
location: 'update', location: 'update',
state: 'another state', state: 'another state',
); );
await tester.pump(); await tester.pump();
expect(find.text('update'), findsOneWidget); expect(find.text('update'), findsOneWidget);
expect(reportedRouteInformation, isNull); expect(reportedRouteInformation!.location, 'update');
expect(reportedRouteInformation!.state, 'another state');
expect(reportedIsNavigation, isFalse);
reportedRouteInformation = null; reportedRouteInformation = null;
reportedIsNavigation = null;
bool result = false; bool result = false;
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false)); result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
expect(result, isTrue); expect(result, isTrue);
await tester.pump(); await tester.pump();
expect(find.text('popped'), findsOneWidget); expect(find.text('popped'), findsOneWidget);
expect(reportedRouteInformation!.location, 'popped'); expect(reportedRouteInformation!.location, 'popped');
expect(reportedIsNavigation, isTrue);
}); });
testWidgets('router can be forced to recognize or ignore navigating events', (WidgetTester tester) async { testWidgets('router can be forced to recognize or ignore navigating events', (WidgetTester tester) async {
RouteInformation? reportedRouteInformation; RouteInformation? reportedRouteInformation;
bool? reportedIsNavigation;
bool isNavigating = false; bool isNavigating = false;
late RouteInformation nextRouteInformation; late RouteInformation nextRouteInformation;
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider( final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(
onRouterReport: (RouteInformation information) { onRouterReport: (RouteInformation information, bool isNavigation) {
// Makes sure we only report once after manually cleaning up. // Makes sure we only report once after manually cleaning up.
expect(reportedRouteInformation, isNull); expect(reportedRouteInformation, isNull);
expect(reportedIsNavigation, isNull);
reportedRouteInformation = information; reportedRouteInformation = information;
reportedIsNavigation = isNavigation;
}, },
); );
provider.value = const RouteInformation( provider.value = const RouteInformation(
...@@ -592,7 +604,10 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester ...@@ -592,7 +604,10 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester
await tester.pump(); await tester.pump();
expect(find.text('initial'), findsNothing); expect(find.text('initial'), findsNothing);
expect(find.text('update'), findsOneWidget); expect(find.text('update'), findsOneWidget);
expect(reportedRouteInformation, isNull); expect(reportedIsNavigation, isFalse);
expect(reportedRouteInformation!.location, 'update');
reportedIsNavigation = null;
reportedRouteInformation = null;
isNavigating = true; isNavigating = true;
// This should not trigger any real navigating event because the // This should not trigger any real navigating event because the
...@@ -600,13 +615,112 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester ...@@ -600,13 +615,112 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester
// report a route information because isNavigating = true. // report a route information because isNavigating = true.
await tester.tap(find.byType(ElevatedButton)); await tester.tap(find.byType(ElevatedButton));
await tester.pump(); await tester.pump();
expect(reportedIsNavigation, isTrue);
expect(reportedRouteInformation!.location, 'update'); expect(reportedRouteInformation!.location, 'update');
reportedIsNavigation = null;
reportedRouteInformation = null;
});
testWidgets('router ignore navigating events updates RouteInformationProvider', (WidgetTester tester) async {
RouteInformation? updatedRouteInformation;
late RouteInformation nextRouteInformation;
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(
onRouterReport: (RouteInformation information, bool isNavigation) {
// This should never be a navigation event.
expect(isNavigation, false);
expect(updatedRouteInformation, isNull);
updatedRouteInformation = information;
},
);
provider.value = const RouteInformation(
location: 'initial',
);
final SimpleRouterDelegate delegate = SimpleRouterDelegate(reportConfiguration: true);
delegate.builder = (BuildContext context, RouteInformation? information) {
return ElevatedButton(
child: Text(information!.location!),
onPressed: () {
Router.neglect(context, () {
if (delegate.routeInformation != nextRouteInformation)
delegate.routeInformation = nextRouteInformation;
});
},
);
};
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
),
));
expect(find.text('initial'), findsOneWidget);
expect(updatedRouteInformation, isNull);
nextRouteInformation = const RouteInformation(
location: 'update',
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.text('initial'), findsNothing);
expect(find.text('update'), findsOneWidget);
expect(updatedRouteInformation!.location, 'update');
});
testWidgets('state change without location changes updates RouteInformationProvider', (WidgetTester tester) async {
RouteInformation? updatedRouteInformation;
late RouteInformation nextRouteInformation;
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(
onRouterReport: (RouteInformation information, bool isNavigation) {
// This should never be a navigation event.
expect(isNavigation, false);
expect(updatedRouteInformation, isNull);
updatedRouteInformation = information;
},
);
provider.value = const RouteInformation(
location: 'initial',
state: 'state1',
);
final SimpleRouterDelegate delegate = SimpleRouterDelegate(reportConfiguration: true);
delegate.builder = (BuildContext context, RouteInformation? information) {
return ElevatedButton(
child: Text(information!.location!),
onPressed: () {
delegate.routeInformation = nextRouteInformation;
},
);
};
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
),
));
expect(find.text('initial'), findsOneWidget);
expect(updatedRouteInformation, isNull);
nextRouteInformation = const RouteInformation(
location: 'initial',
state: 'state2',
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(updatedRouteInformation!.location, 'initial');
expect(updatedRouteInformation!.state, 'state2');
}); });
testWidgets('router does not report when route information is up to date with route information provider', (WidgetTester tester) async { testWidgets('router does not report when route information is up to date with route information provider', (WidgetTester tester) async {
RouteInformation? reportedRouteInformation; RouteInformation? reportedRouteInformation;
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider( final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(
onRouterReport: (RouteInformation information) { onRouterReport: (RouteInformation information, bool isNavigation) {
reportedRouteInformation = information; reportedRouteInformation = information;
}, },
); );
...@@ -691,6 +805,38 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester ...@@ -691,6 +805,38 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester
expect(find.text('newTestRouteName'), findsOneWidget); expect(find.text('newTestRouteName'), findsOneWidget);
}); });
testWidgets('PlatformRouteInformationProvider updates route information', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
TestDefaultBinaryMessengerBinding
.instance!
.defaultBinaryMessenger
.setMockMethodCallHandler(
SystemChannels.navigation,
(MethodCall methodCall) async {
log.add(methodCall);
}
);
final RouteInformationProvider provider = PlatformRouteInformationProvider(
initialRouteInformation: const RouteInformation(
location: 'initial',
),
);
log.clear();
provider.routerReportsNewRouteInformation(const RouteInformation(location: 'a', state: true));
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'location': 'a', 'state': true, 'replace': false }),
]);
log.clear();
provider.routerReportsNewRouteInformation(const RouteInformation(location: 'b', state: false), isNavigation: false);
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'location': 'b', 'state': false, 'replace': true }),
]);
});
testWidgets('RootBackButtonDispatcher works', (WidgetTester tester) async { testWidgets('RootBackButtonDispatcher works', (WidgetTester tester) async {
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
final RouteInformationProvider provider = PlatformRouteInformationProvider( final RouteInformationProvider provider = PlatformRouteInformationProvider(
...@@ -1093,7 +1239,7 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester ...@@ -1093,7 +1239,7 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester
testWidgets('Router reports location if it is different from location given by OS', (WidgetTester tester) async { testWidgets('Router reports location if it is different from location given by OS', (WidgetTester tester) async {
final List<RouteInformation> reportedRouteInformation = <RouteInformation>[]; final List<RouteInformation> reportedRouteInformation = <RouteInformation>[];
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider( final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(
onRouterReport: reportedRouteInformation.add, onRouterReport: (RouteInformation info, bool isNavigation) => reportedRouteInformation.add(info),
)..value = const RouteInformation(location: '/home'); )..value = const RouteInformation(location: '/home');
await tester.pumpWidget(buildBoilerPlate( await tester.pumpWidget(buildBoilerPlate(
...@@ -1131,7 +1277,7 @@ Widget buildBoilerPlate(Widget child) { ...@@ -1131,7 +1277,7 @@ Widget buildBoilerPlate(Widget child) {
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation?); typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation?);
typedef SimpleRouterDelegatePopRoute = Future<bool> Function(); typedef SimpleRouterDelegatePopRoute = Future<bool> Function();
typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result); typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result);
typedef RouterReportRouterInformation = void Function(RouteInformation); typedef RouterReportRouterInformation = void Function(RouteInformation, bool);
class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> { class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> {
SimpleRouteInformationParser(); SimpleRouteInformationParser();
...@@ -1252,8 +1398,9 @@ class SimpleRouteInformationProvider extends RouteInformationProvider with Chang ...@@ -1252,8 +1398,9 @@ class SimpleRouteInformationProvider extends RouteInformationProvider with Chang
} }
@override @override
void routerReportsNewRouteInformation(RouteInformation routeInformation) { void routerReportsNewRouteInformation(RouteInformation routeInformation, {bool isNavigation = true}) {
onRouterReport?.call(routeInformation); _value = routeInformation;
onRouterReport?.call(routeInformation, isNavigation);
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment