diff --git a/packages/flutter/lib/src/services/system_channels.dart b/packages/flutter/lib/src/services/system_channels.dart index f7b9022f4d..456b67a13d 100644 --- a/packages/flutter/lib/src/services/system_channels.dart +++ b/packages/flutter/lib/src/services/system_channels.dart @@ -27,6 +27,24 @@ class SystemChannels { /// * [WidgetsBindingObserver.didPopRoute] and /// [WidgetsBindingObserver.didPushRoute], which expose this channel's /// methods. + /// + /// The following methods are used for the opposite direction data flow. The + /// framework notifies the engine about the route changes. + /// + /// * `routePushed`, which is called when a route is pushed. (e.g. A modal + /// replaces the entire screen.) + /// + /// * `routePopped`, which is called when a route is popped. (e.g. A dialog, + /// such as time picker is closed.) + /// + /// * `routeReplaced`, which is called when a route is replaced. + /// + /// See also: + /// + /// * [Navigator] which manages transitions from one page to another. + /// [Navigator.push], [Navigator.pushReplacement], [Navigator.pop] and + /// [Navigator.replace], utilize this channel's methods to send route + /// change information from framework to engine. static const MethodChannel navigation = MethodChannel( 'flutter/navigation', JSONMethodCodec(), diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index a8e843cfa4..7c7d73cc65 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -9,6 +9,7 @@ import 'dart:developer' as developer; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; import 'basic.dart'; import 'binding.dart'; @@ -16,6 +17,7 @@ import 'focus_manager.dart'; import 'focus_scope.dart'; import 'framework.dart'; import 'overlay.dart'; +import 'route_notification_messages.dart'; import 'routes.dart'; import 'ticker_provider.dart'; @@ -66,6 +68,18 @@ enum RoutePopDisposition { bubble, } +/// Name for the method which is used for sending messages from framework to +/// engine after a route is popped. +const String _routePoppedMethod = 'routePopped'; + +/// Name for the method which is used for sending messages from framework to +/// engine after a route is pushed. +const String _routePushedMethod = 'routePushed'; + +/// Name for the method which is used for sending messages from framework to +/// engine after a route is replaced. +const String _routeReplacedMethod = 'routeReplaced'; + /// An abstraction for an entry managed by a [Navigator]. /// /// This class defines an abstract interface between the navigator and the @@ -1761,6 +1775,7 @@ class NavigatorState extends State with TickerProviderStateMixin { } for (NavigatorObserver observer in widget.observers) observer.didPush(route, oldRoute); + RouteNotificationMessages.maybeNotifyRouteChange(_routePushedMethod, route, oldRoute); assert(() { _debugLocked = false; return true; }()); _afterNavigation(route); return route.popped; @@ -1854,6 +1869,7 @@ class NavigatorState extends State with TickerProviderStateMixin { } for (NavigatorObserver observer in widget.observers) observer.didReplace(newRoute: newRoute, oldRoute: oldRoute); + RouteNotificationMessages.maybeNotifyRouteChange(_routeReplacedMethod, newRoute, oldRoute); assert(() { _debugLocked = false; return true; }()); _afterNavigation(newRoute); return newRoute.popped; @@ -1965,6 +1981,7 @@ class NavigatorState extends State with TickerProviderStateMixin { } for (NavigatorObserver observer in widget.observers) observer.didReplace(newRoute: newRoute, oldRoute: oldRoute); + RouteNotificationMessages.maybeNotifyRouteChange(_routeReplacedMethod, newRoute, oldRoute); oldRoute.dispose(); assert(() { _debugLocked = false; return true; }()); } @@ -2067,6 +2084,7 @@ class NavigatorState extends State with TickerProviderStateMixin { _history.last.didPopNext(route); for (NavigatorObserver observer in widget.observers) observer.didPop(route, _history.last); + RouteNotificationMessages.maybeNotifyRouteChange(_routePoppedMethod, route, _history.last); } else { assert(() { _debugLocked = false; return true; }()); return false; diff --git a/packages/flutter/lib/src/widgets/route_notification_messages.dart b/packages/flutter/lib/src/widgets/route_notification_messages.dart new file mode 100644 index 0000000000..5cbe6bd5eb --- /dev/null +++ b/packages/flutter/lib/src/widgets/route_notification_messages.dart @@ -0,0 +1,42 @@ +// Copyright 2015 The Chromium 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/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'navigator.dart'; + +/// Messages for route change notifications. +class RouteNotificationMessages { + RouteNotificationMessages._(); + + /// When the engine is Web notify the platform for a route change. + static void maybeNotifyRouteChange(String methodName, Route route, Route previousRoute) { + if(kIsWeb) { + _notifyRouteChange(methodName, route, previousRoute); + } else { + // No op. + } + } + + /// Notifies the platform of a route change. + /// + /// There are three methods: 'routePushed', 'routePopped', 'routeReplaced'. + /// + /// See also [SystemChannels.navigation], which handles subsequent navigation + /// requests. + static void _notifyRouteChange(String methodName, Route route, Route previousRoute) { + final String previousRouteName = previousRoute?.settings?.name; + final String routeName = route?.settings?.name; + if (previousRouteName != null || routeName != null) { + SystemChannels.navigation.invokeMethod( + methodName, + { + 'previousRouteName': previousRouteName, + 'routeName': routeName, + }, + ); + } + } +} diff --git a/packages/flutter/test/widgets/route_notification_messages_test.dart b/packages/flutter/test/widgets/route_notification_messages_test.dart new file mode 100644 index 0000000000..32f592734d --- /dev/null +++ b/packages/flutter/test/widgets/route_notification_messages_test.dart @@ -0,0 +1,174 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('chrome') + +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class OnTapPage extends StatelessWidget { + const OnTapPage({Key key, this.id, this.onTap}) : super(key: key); + + final String id; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Page $id')), + body: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + child: Center( + child: Text(id, style: Theme.of(context).textTheme.display2), + ), + ), + ), + ); + } +} + +void main() { + testWidgets('Push and Pop should send platform messages', + (WidgetTester tester) async { + final Map routes = { + '/': (BuildContext context) => OnTapPage( + id: '/', + onTap: () { + Navigator.pushNamed(context, '/A'); + }), + '/A': (BuildContext context) => OnTapPage( + id: 'A', + onTap: () { + Navigator.pop(context); + }), + }; + + final List log = []; + + SystemChannels.navigation + .setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + }); + + await tester.pumpWidget(MaterialApp( + routes: routes, + )); + + expect(log, hasLength(1)); + expect( + log.last, + isMethodCall( + 'routePushed', + arguments: { + 'previousRouteName': null, + 'routeName': '/' + }, + )); + + await tester.tap(find.text('/')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(log, hasLength(2)); + expect( + log.last, + isMethodCall( + 'routePushed', + arguments: { + 'previousRouteName': '/', + 'routeName': '/A' + }, + )); + + await tester.tap(find.text('A')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(log, hasLength(3)); + expect( + log.last, + isMethodCall( + 'routePopped', + arguments: { + 'previousRouteName': '/', + 'routeName': '/A' + }, + )); + }); + + testWidgets('Replace should send platform messages', + (WidgetTester tester) async { + final Map routes = { + '/': (BuildContext context) => OnTapPage( + id: '/', + onTap: () { + Navigator.pushNamed(context, '/A'); + }), + '/A': (BuildContext context) => OnTapPage( + id: 'A', + onTap: () { + Navigator.pushReplacementNamed(context, '/B'); + }), + '/B': (BuildContext context) => OnTapPage(id: 'B', onTap: () {}), + }; + + final List log = []; + + SystemChannels.navigation + .setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + }); + + await tester.pumpWidget(MaterialApp( + routes: routes, + )); + + expect(log, hasLength(1)); + expect( + log.last, + isMethodCall( + 'routePushed', + arguments: { + 'previousRouteName': null, + 'routeName': '/' + }, + )); + + await tester.tap(find.text('/')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(log, hasLength(2)); + expect( + log.last, + isMethodCall( + 'routePushed', + arguments: { + 'previousRouteName': '/', + 'routeName': '/A' + }, + )); + + await tester.tap(find.text('A')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(log, hasLength(3)); + expect( + log.last, + isMethodCall( + 'routeReplaced', + arguments: { + 'previousRouteName': '/A', + 'routeName': '/B' + }, + )); + }); +} diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index dbe67e9d33..88e1ccd9bd 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -808,6 +808,10 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { final String assetFolderPath = Platform.environment['UNIT_TEST_ASSETS']; final String prefix = 'packages/${Platform.environment['APP_NAME']}/'; + /// Navigation related actions (pop, push, replace) broadcasts these actions via + /// platform messages. + SystemChannels.navigation.setMockMethodCallHandler((MethodCall methodCall) async {}); + defaultBinaryMessenger.setMockMessageHandler('flutter/assets', (ByteData message) { String key = utf8.decode(message.buffer.asUint8List()); File asset = File(path.join(assetFolderPath, key));