Unverified Commit 058a9343 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Migrate the navigator API to routeInformationUpdated. (#82594)

parent b9645bfd
...@@ -32,10 +32,19 @@ class SystemChannels { ...@@ -32,10 +32,19 @@ class SystemChannels {
/// The following methods are used for the opposite direction data flow. The /// The following methods are used for the opposite direction data flow. The
/// framework notifies the engine about the route changes. /// framework notifies the engine about the route changes.
/// ///
/// * `routeUpdated`, which is called when current route has changed. /// * `selectSingleEntryHistory`, which enables a single-entry history mode.
/// ///
/// * `routeInformationUpdated`, which is called by the [Router] when the /// * `selectMultiEntryHistory`, which enables a multiple-entry history mode.
/// application navigate to a new location. ///
/// * `routeInformationUpdated`, which is called when the application
/// navigates to a new location, and which takes two arguments, `location`
/// (a URL) and `state` (an object).
///
/// * `routeUpdated`, a deprecated API which can be called in the same
/// situations as `routeInformationUpdated` but whose arguments are
/// `routeName` (a URL) and `previousRouteName` (which is ignored).
///
/// These APIs are exposed by the [SystemNavigator] class.
/// ///
/// See also: /// See also:
/// ///
......
...@@ -33,15 +33,50 @@ class SystemNavigator { ...@@ -33,15 +33,50 @@ class SystemNavigator {
await SystemChannels.platform.invokeMethod<void>('SystemNavigator.pop', animated); await SystemChannels.platform.invokeMethod<void>('SystemNavigator.pop', animated);
} }
/// Selects the single-entry history mode.
///
/// On web, this switches the browser history model to one that only tracks a
/// single entry, so that calling [routeInformationUpdated] replaces the
/// current entry.
///
/// Currently, this is ignored on other platforms.
///
/// See also:
///
/// * [selectMultiEntryHistory], which enables the browser history to have
/// multiple entries.
static Future<void> selectSingleEntryHistory() {
return SystemChannels.navigation.invokeMethod<void>('selectSingleEntryHistory');
}
/// Selects the multiple-entry history mode.
///
/// On web, this switches the browser history model to one that tracks alll
/// updates to [routeInformationUpdated] to form a history stack. This is the
/// default.
///
/// Currently, this is ignored on other platforms.
///
/// See also:
///
/// * [selectSingleEntryHistory], which forces the history to only have one
/// entry.
static Future<void> selectMultiEntryHistory() {
return SystemChannels.navigation.invokeMethod<void>('selectMultiEntryHistory');
}
/// 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, creates a new browser history entry and update URL with the route
/// information. /// information. Whether the history holds one entry or multiple entries is
static void routeInformationUpdated({ /// determined by [selectSingleEntryHistory] and [selectMultiEntryHistory].
///
/// Currently, this is ignored on other platforms.
static Future<void> routeInformationUpdated({
required String location, required String location,
Object? state, Object? state,
}) { }) {
SystemChannels.navigation.invokeMethod<void>( return SystemChannels.navigation.invokeMethod<void>(
'routeInformationUpdated', 'routeInformationUpdated',
<String, dynamic>{ <String, dynamic>{
'location': location, 'location': location,
...@@ -50,14 +85,22 @@ class SystemNavigator { ...@@ -50,14 +85,22 @@ class SystemNavigator {
); );
} }
/// Notifies the platform of a route change. /// Notifies the platform of a route change, and selects single-entry history
/// mode.
///
/// This is equivalent to calling [selectSingleEntryHistory] and
/// [routeInformationUpdated] together.
/// ///
/// On Web, updates the URL bar with the [routeName]. /// The `previousRouteName` argument is ignored.
static void routeUpdated({ @Deprecated(
'Use routeInformationUpdated instead. '
'This feature was deprecated after v2.3.0-1.0.pre.'
)
static Future<void> routeUpdated({
String? routeName, String? routeName,
String? previousRouteName, String? previousRouteName,
}) { }) {
SystemChannels.navigation.invokeMethod<void>( return SystemChannels.navigation.invokeMethod<void>(
'routeUpdated', 'routeUpdated',
<String, dynamic>{ <String, dynamic>{
'previousRouteName': previousRouteName, 'previousRouteName': previousRouteName,
......
...@@ -1624,6 +1624,13 @@ class Navigator extends StatefulWidget { ...@@ -1624,6 +1624,13 @@ class Navigator extends StatefulWidget {
/// route update message to the engine when it detects top-most route changes. /// route update message to the engine when it detects top-most route changes.
/// The messages are used by the web engine to update the browser URL bar. /// The messages are used by the web engine to update the browser URL bar.
/// ///
/// If the property is set to true when the [Navigator] is first created,
/// single-entry history mode is requested using
/// [SystemNavigator.selectSingleEntryHistory]. This means this property
/// should not be used at the same time as [PlatformRouteInformationProvider]
/// is used with a [Router] (including when used with [MaterialApp.router],
/// for example).
///
/// If there are multiple navigators in the widget tree, at most one of them /// If there are multiple navigators in the widget tree, at most one of them
/// can set this property to true (typically, the top-most one created from /// can set this property to true (typically, the top-most one created from
/// the [WidgetsApp]). Otherwise, the web engine may receive multiple route /// the [WidgetsApp]). Otherwise, the web engine may receive multiple route
...@@ -3397,6 +3404,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res ...@@ -3397,6 +3404,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
.getElementForInheritedWidgetOfExactType<HeroControllerScope>() .getElementForInheritedWidgetOfExactType<HeroControllerScope>()
?.widget as HeroControllerScope?; ?.widget as HeroControllerScope?;
_updateHeroController(heroControllerScope?.controller); _updateHeroController(heroControllerScope?.controller);
if (widget.reportsRouteUpdateToEngine) {
SystemNavigator.selectSingleEntryHistory();
}
} }
// Use [_nextPagelessRestorationScopeId] to get the next id. // Use [_nextPagelessRestorationScopeId] to get the next id.
...@@ -4062,17 +4073,14 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res ...@@ -4062,17 +4073,14 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
// notifications. // notifications.
_flushRouteAnnouncement(); _flushRouteAnnouncement();
// Announces route name changes. // Announce route name changes.
if (widget.reportsRouteUpdateToEngine) { if (widget.reportsRouteUpdateToEngine) {
final _RouteEntry? lastEntry = _history.cast<_RouteEntry?>().lastWhere( final _RouteEntry? lastEntry = _history.cast<_RouteEntry?>().lastWhere(
(_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e), orElse: () => null, (_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e), orElse: () => null,
); );
final String? routeName = lastEntry?.route.settings.name; final String? routeName = lastEntry?.route.settings.name;
if (routeName != _lastAnnouncedRouteName) { if (routeName != null && routeName != _lastAnnouncedRouteName) {
SystemNavigator.routeUpdated( SystemNavigator.routeInformationUpdated(location: routeName);
routeName: routeName,
previousRouteName: _lastAnnouncedRouteName,
);
_lastAnnouncedRouteName = routeName; _lastAnnouncedRouteName = routeName;
} }
} }
......
...@@ -1318,6 +1318,11 @@ abstract class RouteInformationProvider extends ValueListenable<RouteInformation ...@@ -1318,6 +1318,11 @@ abstract class RouteInformationProvider extends ValueListenable<RouteInformation
/// This provider also reports the new route information from the [Router] widget /// This provider also reports the new route information from the [Router] widget
/// back to engine using message channel method, the /// back to engine using message channel method, the
/// [SystemNavigator.routeInformationUpdated]. /// [SystemNavigator.routeInformationUpdated].
///
/// Each time [SystemNavigator.routeInformationUpdated] is called, the
/// [SystemNavigator.selectMultiEntryHistory] method is also called. This
/// overrides the initialization behavior of
/// [Navigator.reportsRouteUpdateToEngine].
class PlatformRouteInformationProvider extends RouteInformationProvider with WidgetsBindingObserver, ChangeNotifier { class PlatformRouteInformationProvider extends RouteInformationProvider with WidgetsBindingObserver, ChangeNotifier {
/// Create a platform route information provider. /// Create a platform route information provider.
/// ///
...@@ -1329,6 +1334,7 @@ class PlatformRouteInformationProvider extends RouteInformationProvider with Wid ...@@ -1329,6 +1334,7 @@ class PlatformRouteInformationProvider extends RouteInformationProvider with Wid
@override @override
void routerReportsNewRouteInformation(RouteInformation routeInformation) { void routerReportsNewRouteInformation(RouteInformation routeInformation) {
SystemNavigator.selectMultiEntryHistory();
SystemNavigator.routeInformationUpdated( SystemNavigator.routeInformationUpdated(
location: routeInformation.location!, location: routeInformation.location!,
state: routeInformation.state, state: routeInformation.state,
......
...@@ -2,23 +2,58 @@ ...@@ -2,23 +2,58 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
test('System navigator control test', () async { final List<MethodCall> log = <MethodCall>[];
final List<MethodCall> log = <MethodCall>[];
Future<void> verify(AsyncCallback test, List<Object> expectations) async {
log.clear();
await test();
expect(log, expectations);
}
test('System navigator control test - platform messages', () async {
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async { SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall); log.add(methodCall);
}); });
await SystemNavigator.pop(); await verify(() => SystemNavigator.pop(), <Object>[
isMethodCall('SystemNavigator.pop', arguments: null),
]);
SystemChannels.platform.setMockMethodCallHandler(null);
});
test('System navigator control test - navigation messages', () async {
SystemChannels.navigation.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
await verify(() => SystemNavigator.selectSingleEntryHistory(), <Object>[
isMethodCall('selectSingleEntryHistory', arguments: null),
]);
await verify(() => SystemNavigator.selectMultiEntryHistory(), <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
]);
await verify(() => SystemNavigator.routeInformationUpdated(location: 'a'), <Object>[
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'location': 'a', 'state': null }),
]);
await verify(() => SystemNavigator.routeInformationUpdated(location: 'a', state: true), <Object>[
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'location': 'a', 'state': true }),
]);
await verify(() => SystemNavigator.routeUpdated(routeName: 'a', previousRouteName: 'b'), <Object>[
isMethodCall('routeUpdated', arguments: <String, dynamic>{ 'routeName': 'a', 'previousRouteName': 'b' }),
]);
expect(log, hasLength(1)); SystemChannels.navigation.setMockMethodCallHandler(null);
expect(log.single, isMethodCall('SystemNavigator.pop', arguments: null));
}); });
} }
...@@ -63,46 +63,46 @@ void main() { ...@@ -63,46 +63,46 @@ void main() {
routes: routes, routes: routes,
)); ));
expect(log, hasLength(1)); expect(log, <Object>[
expect( isMethodCall('selectSingleEntryHistory', arguments: null),
log.last, isMethodCall('routeInformationUpdated',
isMethodCall(
'routeUpdated',
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'previousRouteName': null, 'location': '/',
'routeName': '/', 'state': null,
}, },
), ),
); ]);
log.clear();
await tester.tap(find.text('/')); await tester.tap(find.text('/'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(log, hasLength(2)); expect(log, hasLength(1));
expect( expect(
log.last, log.last,
isMethodCall( isMethodCall(
'routeUpdated', 'routeInformationUpdated',
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'previousRouteName': '/', 'location': '/A',
'routeName': '/A', 'state': null,
}, },
), ),
); );
log.clear();
await tester.tap(find.text('A')); await tester.tap(find.text('A'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(log, hasLength(3)); expect(log, hasLength(1));
expect( expect(
log.last, log.last,
isMethodCall( isMethodCall(
'routeUpdated', 'routeInformationUpdated',
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'previousRouteName': '/A', 'location': '/',
'routeName': '/', 'state': null,
}, },
), ),
); );
...@@ -168,46 +168,46 @@ void main() { ...@@ -168,46 +168,46 @@ void main() {
routes: routes, routes: routes,
)); ));
expect(log, hasLength(1)); expect(log, <Object>[
expect( isMethodCall('selectSingleEntryHistory', arguments: null),
log.last, isMethodCall('routeInformationUpdated',
isMethodCall(
'routeUpdated',
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'previousRouteName': null, 'location': '/',
'routeName': '/', 'state': null,
}, },
), ),
); ]);
log.clear();
await tester.tap(find.text('/')); await tester.tap(find.text('/'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(log, hasLength(2)); expect(log, hasLength(1));
expect( expect(
log.last, log.last,
isMethodCall( isMethodCall(
'routeUpdated', 'routeInformationUpdated',
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'previousRouteName': '/', 'location': '/A',
'routeName': '/A', 'state': null,
}, },
), ),
); );
log.clear();
await tester.tap(find.text('A')); await tester.tap(find.text('A'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(log, hasLength(3)); expect(log, hasLength(1));
expect( expect(
log.last, log.last,
isMethodCall( isMethodCall(
'routeUpdated', 'routeInformationUpdated',
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'previousRouteName': '/A', 'location': '/B',
'routeName': '/B', 'state': null,
}, },
), ),
); );
...@@ -237,27 +237,22 @@ void main() { ...@@ -237,27 +237,22 @@ void main() {
}, },
)); ));
expect(log, hasLength(1)); expect(log, <Object>[
expect( isMethodCall('selectSingleEntryHistory', arguments: null),
log.last, isMethodCall('routeInformationUpdated',
isMethodCall('routeUpdated', arguments: <String, dynamic>{ arguments: <String, dynamic>{
'previousRouteName': null, 'location': '/home',
'routeName': '/home', 'state': null,
}), },
); ),
]);
log.clear();
await tester.tap(find.text('Home')); await tester.tap(find.text('Home'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(log, hasLength(2)); expect(log, isEmpty);
expect(
log.last,
isMethodCall('routeUpdated', arguments: <String, dynamic>{
'previousRouteName': '/home',
'routeName': null,
}),
);
}); });
testWidgets('PlatformRouteInformationProvider reports URL', (WidgetTester tester) async { testWidgets('PlatformRouteInformationProvider reports URL', (WidgetTester tester) async {
...@@ -294,16 +289,13 @@ void main() { ...@@ -294,16 +289,13 @@ void main() {
await tester.pump(); await tester.pump();
expect(find.text('update'), findsOneWidget); expect(find.text('update'), findsOneWidget);
expect(log, hasLength(1)); expect(log, <Object>[
// TODO(chunhtai): check routeInformationUpdated instead once the engine isMethodCall('selectMultiEntryHistory', arguments: null),
// side is done.
expect(
log.last,
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
'location': 'update', 'location': 'update',
'state': 'state', 'state': 'state',
}), }),
); ]);
}); });
} }
......
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