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

Implement Router widget and widgets app api (#60299)

parent 12b8d9db
...@@ -104,6 +104,50 @@ class CupertinoApp extends StatefulWidget { ...@@ -104,6 +104,50 @@ class CupertinoApp extends StatefulWidget {
assert(checkerboardOffscreenLayers != null), assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null), assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null), assert(debugShowCheckedModeBanner != null),
routeInformationProvider = null,
routeInformationParser = null,
routerDelegate = null,
backButtonDispatcher = null,
super(key: key);
/// Creates a [CupertinoApp] that uses the [Router] instead of a [Navigator].
const CupertinoApp.router({
Key key,
this.routeInformationProvider,
@required this.routeInformationParser,
@required this.routerDelegate,
this.backButtonDispatcher,
this.theme,
this.builder,
this.title = '',
this.onGenerateTitle,
this.color,
this.locale,
this.localizationsDelegates,
this.localeListResolutionCallback,
this.localeResolutionCallback,
this.supportedLocales = const <Locale>[Locale('en', 'US')],
this.showPerformanceOverlay = false,
this.checkerboardRasterCacheImages = false,
this.checkerboardOffscreenLayers = false,
this.showSemanticsDebugger = false,
this.debugShowCheckedModeBanner = true,
this.shortcuts,
this.actions,
}) : assert(title != null),
assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null),
assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null),
navigatorObservers = null,
navigatorKey = null,
onGenerateRoute = null,
home = null,
onGenerateInitialRoutes = null,
onUnknownRoute = null,
routes = null,
initialRoute = null,
super(key: key); super(key: key);
/// {@macro flutter.widgets.widgetsApp.navigatorKey} /// {@macro flutter.widgets.widgetsApp.navigatorKey}
...@@ -143,6 +187,18 @@ class CupertinoApp extends StatefulWidget { ...@@ -143,6 +187,18 @@ class CupertinoApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.navigatorObservers} /// {@macro flutter.widgets.widgetsApp.navigatorObservers}
final List<NavigatorObserver> navigatorObservers; final List<NavigatorObserver> navigatorObservers;
/// {@macro flutter.widgets.widgetsApp.routeInformationProvider}
final RouteInformationProvider routeInformationProvider;
/// {@macro flutter.widgets.widgetsApp.routeInformationParser}
final RouteInformationParser<Object> routeInformationParser;
/// {@macro flutter.widgets.widgetsApp.routerDelegate}
final RouterDelegate<Object> routerDelegate;
/// {@macro flutter.widgets.widgetsApp.backButtonDispatcher}
final BackButtonDispatcher backButtonDispatcher;
/// {@macro flutter.widgets.widgetsApp.builder} /// {@macro flutter.widgets.widgetsApp.builder}
final TransitionBuilder builder; final TransitionBuilder builder;
...@@ -286,6 +342,7 @@ class _AlwaysCupertinoScrollBehavior extends ScrollBehavior { ...@@ -286,6 +342,7 @@ class _AlwaysCupertinoScrollBehavior extends ScrollBehavior {
class _CupertinoAppState extends State<CupertinoApp> { class _CupertinoAppState extends State<CupertinoApp> {
HeroController _heroController; HeroController _heroController;
bool get _usesRouter => widget.routerDelegate != null;
@override @override
void initState() { void initState() {
...@@ -304,6 +361,83 @@ class _CupertinoAppState extends State<CupertinoApp> { ...@@ -304,6 +361,83 @@ class _CupertinoAppState extends State<CupertinoApp> {
yield DefaultCupertinoLocalizations.delegate; yield DefaultCupertinoLocalizations.delegate;
} }
Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return CupertinoButton.filled(
child: const Icon(
CupertinoIcons.search,
size: 28.0,
color: CupertinoColors.white,
),
padding: EdgeInsets.zero,
onPressed: onPressed,
);
}
WidgetsApp _buildWidgetApp(BuildContext context) {
final CupertinoThemeData effectiveThemeData = CupertinoTheme.of(context);
final Color color = CupertinoDynamicColor.resolve(widget.color ?? effectiveThemeData.primaryColor, context);
if (_usesRouter) {
return WidgetsApp.router(
key: GlobalObjectKey(this),
routeInformationProvider: widget.routeInformationProvider,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate,
backButtonDispatcher: widget.backButtonDispatcher,
builder: widget.builder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: effectiveThemeData.textTheme.textStyle,
color: color,
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
localeListResolutionCallback: widget.localeListResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts,
actions: widget.actions,
);
}
return WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: widget.navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
return CupertinoPageRoute<T>(settings: settings, builder: builder);
},
home: widget.home,
routes: widget.routes,
initialRoute: widget.initialRoute,
onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute,
builder: widget.builder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: effectiveThemeData.textTheme.textStyle,
color: color,
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
localeListResolutionCallback: widget.localeListResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts,
actions: widget.actions,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData(); final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
...@@ -314,53 +448,11 @@ class _CupertinoAppState extends State<CupertinoApp> { ...@@ -314,53 +448,11 @@ class _CupertinoAppState extends State<CupertinoApp> {
data: CupertinoUserInterfaceLevelData.base, data: CupertinoUserInterfaceLevelData.base,
child: CupertinoTheme( child: CupertinoTheme(
data: effectiveThemeData, data: effectiveThemeData,
child: Builder( child: HeroControllerScope(
builder: (BuildContext context) { controller: _heroController,
return HeroControllerScope( child: Builder(
controller: _heroController, builder: _buildWidgetApp,
child: WidgetsApp( ),
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: widget.navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) =>
CupertinoPageRoute<T>(settings: settings, builder: builder),
home: widget.home,
routes: widget.routes,
initialRoute: widget.initialRoute,
onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute,
builder: widget.builder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: CupertinoTheme.of(context).textTheme.textStyle,
color: CupertinoDynamicColor.resolve(widget.color ?? effectiveThemeData.primaryColor, context),
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
localeListResolutionCallback: widget.localeListResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) {
return CupertinoButton.filled(
child: const Icon(
CupertinoIcons.search,
size: 28.0,
color: CupertinoColors.white,
),
padding: EdgeInsets.zero,
onPressed: onPressed,
);
},
shortcuts: widget.shortcuts,
actions: widget.actions,
),
);
},
), ),
), ),
), ),
......
This diff is collapsed.
...@@ -26,16 +26,18 @@ class SystemChannels { ...@@ -26,16 +26,18 @@ class SystemChannels {
/// * `pushRoute`, which is called with a single string argument when the /// * `pushRoute`, which is called with a single string argument when the
/// operating system instructs the application to open a particular page. /// operating system instructs the application to open a particular page.
/// ///
/// * `pushRouteInformation`, which is called with a map, which contains a
/// location string and a state object, when the operating system instructs
/// the application to open a particular page. These parameters are stored
/// under the key `location` and `state` in the map.
///
/// 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.
/// ///
/// * `routePushed`, which is called when a route is pushed. (e.g. A modal /// * `routeUpdated`, which is called when current route has changed.
/// 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. /// * `routeInformationUpdated`, which is called by the [Router] when the
/// application navigate to a new location.
/// ///
/// See also: /// See also:
/// ///
...@@ -46,7 +48,7 @@ class SystemChannels { ...@@ -46,7 +48,7 @@ class SystemChannels {
/// [Navigator.push], [Navigator.pushReplacement], [Navigator.pop] and /// [Navigator.push], [Navigator.pushReplacement], [Navigator.pop] and
/// [Navigator.replace], utilize this channel's methods to send route /// [Navigator.replace], utilize this channel's methods to send route
/// change information from framework to engine. /// change information from framework to engine.
static const MethodChannel navigation = MethodChannel( static const MethodChannel navigation = OptionalMethodChannel(
'flutter/navigation', 'flutter/navigation',
JSONMethodCodec(), JSONMethodCodec(),
); );
......
...@@ -35,4 +35,37 @@ class SystemNavigator { ...@@ -35,4 +35,37 @@ class SystemNavigator {
static Future<void> pop({bool? animated}) async { static Future<void> pop({bool? animated}) async {
await SystemChannels.platform.invokeMethod<void>('SystemNavigator.pop', animated); await SystemChannels.platform.invokeMethod<void>('SystemNavigator.pop', animated);
} }
/// Notifies the platform for a route information change.
///
/// On Web, creates a new browser history entry and update URL with the route
/// information.
static void routeInformationUpdated({
required String location,
Object? state
}) {
SystemChannels.navigation.invokeMethod<void>(
'routeInformationUpdated',
<String, dynamic>{
'location': location,
'state': state,
},
);
}
/// Notifies the platform of a route change.
///
/// On Web, updates the URL bar with the [routeName].
static void routeUpdated({
String? routeName,
String? previousRouteName
}) {
SystemChannels.navigation.invokeMethod<void>(
'routeUpdated',
<String, dynamic>{
'previousRouteName': previousRouteName,
'routeName': routeName,
},
);
}
} }
This diff is collapsed.
...@@ -18,6 +18,7 @@ import 'app.dart'; ...@@ -18,6 +18,7 @@ import 'app.dart';
import 'debug.dart'; import 'debug.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'framework.dart'; import 'framework.dart';
import 'router.dart';
import 'widget_inspector.dart'; import 'widget_inspector.dart';
export 'dart:ui' show AppLifecycleState, Locale; export 'dart:ui' show AppLifecycleState, Locale;
...@@ -95,7 +96,7 @@ abstract class WidgetsBindingObserver { ...@@ -95,7 +96,7 @@ abstract class WidgetsBindingObserver {
/// [SystemChannels.navigation]. /// [SystemChannels.navigation].
Future<bool> didPopRoute() => Future<bool>.value(false); Future<bool> didPopRoute() => Future<bool>.value(false);
/// Called when the host tells the app to push a new route onto the /// Called when the host tells the application to push a new route onto the
/// navigator. /// navigator.
/// ///
/// Observers are expected to return true if they were able to /// Observers are expected to return true if they were able to
...@@ -106,6 +107,22 @@ abstract class WidgetsBindingObserver { ...@@ -106,6 +107,22 @@ abstract class WidgetsBindingObserver {
/// [SystemChannels.navigation]. /// [SystemChannels.navigation].
Future<bool> didPushRoute(String route) => Future<bool>.value(false); Future<bool> didPushRoute(String route) => Future<bool>.value(false);
/// Called when the host tells the application to push a new
/// [RouteInformation] and a restoration state onto the router.
///
/// Observers are expected to return true if they were able to
/// handle the notification. Observers are notified in registration
/// order until one returns true.
///
/// This method exposes the `pushRouteInformation` notification from
/// [SystemChannels.navigation].
///
/// The default implementation is to call the [didPushRoute] directly with the
/// [RouteInformation.location].
Future<bool> didPushRouteInformation(RouteInformation routeInformation) {
return didPushRoute(routeInformation.location);
}
/// Called when the application's dimensions change. For example, /// Called when the application's dimensions change. For example,
/// when a phone is rotated. /// when a phone is rotated.
/// ///
...@@ -654,12 +671,28 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -654,12 +671,28 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
} }
} }
Future<void> _handlePushRouteInformation(Map<dynamic, dynamic> routeArguments) async {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.from(_observers)) {
if (
await observer.didPushRouteInformation(
RouteInformation(
location: routeArguments['location'] as String,
state: routeArguments['state'] as Object,
)
)
)
return;
}
}
Future<dynamic> _handleNavigationInvocation(MethodCall methodCall) { Future<dynamic> _handleNavigationInvocation(MethodCall methodCall) {
switch (methodCall.method) { switch (methodCall.method) {
case 'popRoute': case 'popRoute':
return handlePopRoute(); return handlePopRoute();
case 'pushRoute': case 'pushRoute':
return handlePushRoute(methodCall.arguments as String); return handlePushRoute(methodCall.arguments as String);
case 'pushRouteInformation':
return _handlePushRouteInformation(methodCall.arguments as Map<dynamic, dynamic>);
} }
return Future<dynamic>.value(); return Future<dynamic>.value();
} }
......
...@@ -21,7 +21,6 @@ import 'focus_scope.dart'; ...@@ -21,7 +21,6 @@ import 'focus_scope.dart';
import 'framework.dart'; import 'framework.dart';
import 'heroes.dart'; import 'heroes.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'route_notification_messages.dart';
import 'routes.dart'; import 'routes.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
...@@ -3308,8 +3307,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -3308,8 +3307,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_RouteEntry.isPresentPredicate, orElse: () => null); _RouteEntry.isPresentPredicate, orElse: () => null);
final String routeName = lastEntry?.route?.settings?.name; final String routeName = lastEntry?.route?.settings?.name;
if (routeName != _lastAnnouncedRouteName) { if (routeName != _lastAnnouncedRouteName) {
RouteNotificationMessages.maybeNotifyRouteChange( SystemNavigator.routeUpdated(
routeName, _lastAnnouncedRouteName); routeName: routeName,
previousRouteName: _lastAnnouncedRouteName
);
_lastAnnouncedRouteName = routeName; _lastAnnouncedRouteName = routeName;
} }
} }
......
// Copyright 2014 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.
// @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// Messages for route change notifications.
class RouteNotificationMessages {
// This class is not meant to be instantiated or extended; this constructor
// prevents instantiation and extension.
// ignore: unused_element
RouteNotificationMessages._();
/// When the engine is Web notify the platform for a route change.
static void maybeNotifyRouteChange(String routeName, String previousRouteName) {
if(kIsWeb) {
_notifyRouteChange(routeName, previousRouteName);
} else {
// No op.
}
}
/// Notifies the platform of a route change.
///
/// See also:
///
/// * [SystemChannels.navigation], which handles subsequent navigation
/// requests.
static void _notifyRouteChange(String routeName, String previousRouteName) {
SystemChannels.navigation.invokeMethod<void>(
'routeUpdated',
<String, dynamic>{
'previousRouteName': previousRouteName,
'routeName': routeName,
},
);
}
}
This diff is collapsed.
...@@ -85,6 +85,7 @@ export 'src/widgets/primary_scroll_controller.dart'; ...@@ -85,6 +85,7 @@ export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/restoration.dart'; export 'src/widgets/restoration.dart';
export 'src/widgets/restoration_properties.dart'; export 'src/widgets/restoration_properties.dart';
export 'src/widgets/router.dart';
export 'src/widgets/routes.dart'; export 'src/widgets/routes.dart';
export 'src/widgets/safe_area.dart'; export 'src/widgets/safe_area.dart';
export 'src/widgets/scroll_activity.dart'; export 'src/widgets/scroll_activity.dart';
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
void main() { void main() {
testWidgets('Heroes work', (WidgetTester tester) async { testWidgets('Heroes work', (WidgetTester tester) async {
...@@ -147,4 +149,101 @@ void main() { ...@@ -147,4 +149,101 @@ void main() {
expect(key2.currentState, isA<NavigatorState>()); expect(key2.currentState, isA<NavigatorState>());
expect(key1.currentState, isNull); expect(key1.currentState, isNull);
}); });
testWidgets('CupertinoApp.router works', (WidgetTester tester) async {
final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider(
initialRouteInformation: const RouteInformation(
location: 'initial',
),
);
final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
},
onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) {
delegate.routeInformation = const RouteInformation(
location: 'popped',
);
return route.didPop(result);
}
);
await tester.pumpWidget(CupertinoApp.router(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
));
expect(find.text('initial'), findsOneWidget);
// Simulate android back button intent.
final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute'));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
await tester.pumpAndSettle();
expect(find.text('popped'), findsOneWidget);
});
}
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate);
class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> {
SimpleRouteInformationParser();
@override
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
return SynchronousFuture<RouteInformation>(information);
}
@override
RouteInformation restoreRouteInformation(RouteInformation configuration) {
return configuration;
}
}
class SimpleNavigatorRouterDelegate extends RouterDelegate<RouteInformation> with PopNavigatorRouterDelegateMixin<RouteInformation>, ChangeNotifier {
SimpleNavigatorRouterDelegate({
@required this.builder,
this.onPopPage,
});
@override
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
RouteInformation get routeInformation => _routeInformation;
RouteInformation _routeInformation;
set routeInformation(RouteInformation newValue) {
_routeInformation = newValue;
notifyListeners();
}
SimpleRouterDelegateBuilder builder;
SimpleNavigatorRouterDelegatePopPage<void> onPopPage;
@override
Future<void> setNewRoutePath(RouteInformation configuration) {
_routeInformation = configuration;
return SynchronousFuture<void>(null);
}
bool _handlePopPage(Route<void> route, void data) {
return onPopPage(route, data, this);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: <Page<void>>[
// We need at least two pages for the pop to propagate through.
// Otherwise, the navigator will bubble the pop to the system navigator.
CupertinoPage<void>(
builder: (BuildContext context) => const Text('base'),
),
CupertinoPage<void>(
key: ValueKey<String>(routeInformation?.location),
builder: (BuildContext context) => builder(context, routeInformation),
)
],
);
}
} }
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
// @dart = 2.8 // @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
...@@ -947,6 +949,37 @@ void main() { ...@@ -947,6 +949,37 @@ void main() {
expect(key2.currentState, isA<NavigatorState>()); expect(key2.currentState, isA<NavigatorState>());
expect(key1.currentState, isNull); expect(key1.currentState, isNull);
}); });
testWidgets('MaterialApp.router works', (WidgetTester tester) async {
final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider(
initialRouteInformation: const RouteInformation(
location: 'initial',
),
);
final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
},
onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) {
delegate.routeInformation = const RouteInformation(
location: 'popped',
);
return route.didPop(result);
}
);
await tester.pumpWidget(MaterialApp.router(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
));
expect(find.text('initial'), findsOneWidget);
// Simulate android back button intent.
final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute'));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
await tester.pumpAndSettle();
expect(find.text('popped'), findsOneWidget);
});
} }
class MockAccessibilityFeature implements AccessibilityFeatures { class MockAccessibilityFeature implements AccessibilityFeatures {
...@@ -968,3 +1001,69 @@ class MockAccessibilityFeature implements AccessibilityFeatures { ...@@ -968,3 +1001,69 @@ class MockAccessibilityFeature implements AccessibilityFeatures {
@override @override
bool get reduceMotion => true; bool get reduceMotion => true;
} }
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate);
class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> {
SimpleRouteInformationParser();
@override
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
return SynchronousFuture<RouteInformation>(information);
}
@override
RouteInformation restoreRouteInformation(RouteInformation configuration) {
return configuration;
}
}
class SimpleNavigatorRouterDelegate extends RouterDelegate<RouteInformation> with PopNavigatorRouterDelegateMixin<RouteInformation>, ChangeNotifier {
SimpleNavigatorRouterDelegate({
@required this.builder,
this.onPopPage,
});
@override
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
RouteInformation get routeInformation => _routeInformation;
RouteInformation _routeInformation;
set routeInformation(RouteInformation newValue) {
_routeInformation = newValue;
notifyListeners();
}
SimpleRouterDelegateBuilder builder;
SimpleNavigatorRouterDelegatePopPage<void> onPopPage;
@override
Future<void> setNewRoutePath(RouteInformation configuration) {
_routeInformation = configuration;
return SynchronousFuture<void>(null);
}
bool _handlePopPage(Route<void> route, void data) {
return onPopPage(route, data, this);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: <Page<void>>[
// We need at least two pages for the pop to propagate through.
// Otherwise, the navigator will bubble the pop to the system navigator.
MaterialPage<void>(
builder: (BuildContext context) => const Text('base'),
),
MaterialPage<void>(
key: ValueKey<String>(routeInformation?.location),
builder: (BuildContext context) => builder(context, routeInformation),
)
],
);
}
}
...@@ -264,4 +264,116 @@ void main() { ...@@ -264,4 +264,116 @@ void main() {
expect(find.text('non-regular page one'), findsOneWidget); expect(find.text('non-regular page one'), findsOneWidget);
expect(find.text('regular page'), findsNothing); expect(find.text('regular page'), findsNothing);
}); });
testWidgets('WidgetsApp.router works', (WidgetTester tester) async {
final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider(
initialRouteInformation: const RouteInformation(
location: 'initial',
),
);
final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
},
onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) {
delegate.routeInformation = const RouteInformation(
location: 'popped',
);
return route.didPop(result);
}
);
await tester.pumpWidget(WidgetsApp.router(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
color: const Color(0xFF123456),
));
expect(find.text('initial'), findsOneWidget);
// Simulate android back button intent.
final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute'));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
await tester.pumpAndSettle();
expect(find.text('popped'), findsOneWidget);
});
testWidgets('WidgetsApp.router has correct default', (WidgetTester tester) async {
final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
},
);
await tester.pumpWidget(WidgetsApp.router(
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
color: const Color(0xFF123456),
));
expect(find.text('/'), findsOneWidget);
});
}
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate);
class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> {
SimpleRouteInformationParser();
@override
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
return SynchronousFuture<RouteInformation>(information);
}
@override
RouteInformation restoreRouteInformation(RouteInformation configuration) {
return configuration;
}
}
class SimpleNavigatorRouterDelegate extends RouterDelegate<RouteInformation> with PopNavigatorRouterDelegateMixin<RouteInformation>, ChangeNotifier {
SimpleNavigatorRouterDelegate({
@required this.builder,
this.onPopPage,
});
@override
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
RouteInformation get routeInformation => _routeInformation;
RouteInformation _routeInformation;
set routeInformation(RouteInformation newValue) {
_routeInformation = newValue;
notifyListeners();
}
SimpleRouterDelegateBuilder builder;
SimpleNavigatorRouterDelegatePopPage<void> onPopPage;
@override
Future<void> setNewRoutePath(RouteInformation configuration) {
_routeInformation = configuration;
return SynchronousFuture<void>(null);
}
bool _handlePopPage(Route<void> route, void data) {
return onPopPage(route, data, this);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: <Page<void>>[
// We need at least two pages for the pop to propagate through.
// Otherwise, the navigator will bubble the pop to the system navigator.
MaterialPage<void>(
builder: (BuildContext context) => const Text('base'),
),
MaterialPage<void>(
key: ValueKey<String>(routeInformation?.location),
builder: (BuildContext context) => builder(context, routeInformation),
)
],
);
}
} }
...@@ -40,6 +40,16 @@ class PushRouteObserver with WidgetsBindingObserver { ...@@ -40,6 +40,16 @@ class PushRouteObserver with WidgetsBindingObserver {
} }
} }
class PushRouteInformationObserver with WidgetsBindingObserver {
RouteInformation pushedRouteInformation;
@override
Future<bool> didPushRouteInformation(RouteInformation routeInformation) async {
pushedRouteInformation = routeInformation;
return true;
}
}
void main() { void main() {
setUp(() { setUp(() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
...@@ -90,6 +100,38 @@ void main() { ...@@ -90,6 +100,38 @@ void main() {
WidgetsBinding.instance.removeObserver(observer); WidgetsBinding.instance.removeObserver(observer);
}); });
testWidgets('didPushRouteInformation calls didPushRoute by default', (WidgetTester tester) async {
final PushRouteObserver observer = PushRouteObserver();
WidgetsBinding.instance.addObserver(observer);
const Map<String, dynamic> testRouteInformation = <String, dynamic>{
'location': 'testRouteName',
'state': 'state',
'restorationData': <dynamic, dynamic>{'test': 'config'}
};
final ByteData message = const JSONMethodCodec().encodeMethodCall(
const MethodCall('pushRouteInformation', testRouteInformation));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
expect(observer.pushedRoute, 'testRouteName');
WidgetsBinding.instance.removeObserver(observer);
});
testWidgets('didPushRouteInformation callback', (WidgetTester tester) async {
final PushRouteInformationObserver observer = PushRouteInformationObserver();
WidgetsBinding.instance.addObserver(observer);
const Map<String, dynamic> testRouteInformation = <String, dynamic>{
'location': 'testRouteName',
'state': 'state',
};
final ByteData message = const JSONMethodCodec().encodeMethodCall(
const MethodCall('pushRouteInformation', testRouteInformation));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
expect(observer.pushedRouteInformation.location, 'testRouteName');
expect(observer.pushedRouteInformation.state, 'state');
WidgetsBinding.instance.removeObserver(observer);
});
testWidgets('Application lifecycle affects frame scheduling', (WidgetTester tester) async { testWidgets('Application lifecycle affects frame scheduling', (WidgetTester tester) async {
final BinaryMessenger defaultBinaryMessenger = ServicesBinding.instance.defaultBinaryMessenger; final BinaryMessenger defaultBinaryMessenger = ServicesBinding.instance.defaultBinaryMessenger;
ByteData message; ByteData message;
......
...@@ -36,6 +36,13 @@ class OnTapPage extends StatelessWidget { ...@@ -36,6 +36,13 @@ class OnTapPage extends StatelessWidget {
} }
} }
Map<String, dynamic> convertRouteInformationToMap(RouteInformation routeInformation) {
return <String, dynamic>{
'location': routeInformation.location,
'state': routeInformation.state,
};
}
void main() { void main() {
testWidgets('Push and Pop should send platform messages', (WidgetTester tester) async { testWidgets('Push and Pop should send platform messages', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
...@@ -258,4 +265,109 @@ void main() { ...@@ -258,4 +265,109 @@ void main() {
}), }),
); );
}); });
testWidgets('PlatformRouteInformationProvider reports URL', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
SystemChannels.navigation.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider(
initialRouteInformation: const RouteInformation(
location: 'initial',
),
);
final SimpleRouterDelegate delegate = SimpleRouterDelegate(
reportConfiguration: true,
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
}
);
await tester.pumpWidget(MaterialApp.router(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
));
expect(find.text('initial'), findsOneWidget);
// Triggers a router rebuild and verify the route information is reported
// to the web engine.
delegate.routeInformation = const RouteInformation(
location: 'update',
state: 'state',
);
await tester.pump();
expect(find.text('update'), findsOneWidget);
expect(log, hasLength(1));
// TODO(chunhtai): check routeInformationUpdated instead once the engine
// side is done.
expect(
log.last,
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
'location': 'update',
'state': 'state',
}),
);
});
}
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleRouterDelegatePopRoute = Future<bool> Function();
class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> {
SimpleRouteInformationParser();
@override
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
return SynchronousFuture<RouteInformation>(information);
}
@override
RouteInformation restoreRouteInformation(RouteInformation configuration) {
return configuration;
}
}
class SimpleRouterDelegate extends RouterDelegate<RouteInformation> with ChangeNotifier {
SimpleRouterDelegate({
@required this.builder,
this.onPopRoute,
this.reportConfiguration = false,
});
RouteInformation get routeInformation => _routeInformation;
RouteInformation _routeInformation;
set routeInformation(RouteInformation newValue) {
_routeInformation = newValue;
notifyListeners();
}
SimpleRouterDelegateBuilder builder;
SimpleRouterDelegatePopRoute onPopRoute;
final bool reportConfiguration;
@override
RouteInformation get currentConfiguration {
if (reportConfiguration)
return routeInformation;
return null;
}
@override
Future<void> setNewRoutePath(RouteInformation configuration) {
_routeInformation = configuration;
return SynchronousFuture<void>(null);
}
@override
Future<bool> popRoute() {
if (onPopRoute != null)
return onPopRoute();
return SynchronousFuture<bool>(true);
}
@override
Widget build(BuildContext context) => builder(context, routeInformation);
} }
This diff is collapsed.
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