Implement Router widget and widgets app api (#60299)
This commit is contained in:
parent
12b8d9db80
commit
f9fd71bc78
@ -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 {
|
|||||||
/// {@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 {
|
|||||||
|
|
||||||
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,37 +361,34 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
|||||||
yield DefaultCupertinoLocalizations.delegate;
|
yield DefaultCupertinoLocalizations.delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
|
||||||
Widget build(BuildContext context) {
|
return CupertinoButton.filled(
|
||||||
final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
|
child: const Icon(
|
||||||
|
CupertinoIcons.search,
|
||||||
|
size: 28.0,
|
||||||
|
color: CupertinoColors.white,
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: onPressed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return ScrollConfiguration(
|
WidgetsApp _buildWidgetApp(BuildContext context) {
|
||||||
behavior: _AlwaysCupertinoScrollBehavior(),
|
final CupertinoThemeData effectiveThemeData = CupertinoTheme.of(context);
|
||||||
child: CupertinoUserInterfaceLevel(
|
final Color color = CupertinoDynamicColor.resolve(widget.color ?? effectiveThemeData.primaryColor, context);
|
||||||
data: CupertinoUserInterfaceLevelData.base,
|
|
||||||
child: CupertinoTheme(
|
if (_usesRouter) {
|
||||||
data: effectiveThemeData,
|
return WidgetsApp.router(
|
||||||
child: Builder(
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return HeroControllerScope(
|
|
||||||
controller: _heroController,
|
|
||||||
child: WidgetsApp(
|
|
||||||
key: GlobalObjectKey(this),
|
key: GlobalObjectKey(this),
|
||||||
navigatorKey: widget.navigatorKey,
|
routeInformationProvider: widget.routeInformationProvider,
|
||||||
navigatorObservers: widget.navigatorObservers,
|
routeInformationParser: widget.routeInformationParser,
|
||||||
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) =>
|
routerDelegate: widget.routerDelegate,
|
||||||
CupertinoPageRoute<T>(settings: settings, builder: builder),
|
backButtonDispatcher: widget.backButtonDispatcher,
|
||||||
home: widget.home,
|
|
||||||
routes: widget.routes,
|
|
||||||
initialRoute: widget.initialRoute,
|
|
||||||
onGenerateRoute: widget.onGenerateRoute,
|
|
||||||
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
|
|
||||||
onUnknownRoute: widget.onUnknownRoute,
|
|
||||||
builder: widget.builder,
|
builder: widget.builder,
|
||||||
title: widget.title,
|
title: widget.title,
|
||||||
onGenerateTitle: widget.onGenerateTitle,
|
onGenerateTitle: widget.onGenerateTitle,
|
||||||
textStyle: CupertinoTheme.of(context).textTheme.textStyle,
|
textStyle: effectiveThemeData.textTheme.textStyle,
|
||||||
color: CupertinoDynamicColor.resolve(widget.color ?? effectiveThemeData.primaryColor, context),
|
color: color,
|
||||||
locale: widget.locale,
|
locale: widget.locale,
|
||||||
localizationsDelegates: _localizationsDelegates,
|
localizationsDelegates: _localizationsDelegates,
|
||||||
localeResolutionCallback: widget.localeResolutionCallback,
|
localeResolutionCallback: widget.localeResolutionCallback,
|
||||||
@ -345,22 +399,60 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
|||||||
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
|
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
|
||||||
showSemanticsDebugger: widget.showSemanticsDebugger,
|
showSemanticsDebugger: widget.showSemanticsDebugger,
|
||||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||||
inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) {
|
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
|
||||||
return CupertinoButton.filled(
|
|
||||||
child: const Icon(
|
|
||||||
CupertinoIcons.search,
|
|
||||||
size: 28.0,
|
|
||||||
color: CupertinoColors.white,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
onPressed: onPressed,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
shortcuts: widget.shortcuts,
|
shortcuts: widget.shortcuts,
|
||||||
actions: widget.actions,
|
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
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
|
||||||
|
|
||||||
|
return ScrollConfiguration(
|
||||||
|
behavior: _AlwaysCupertinoScrollBehavior(),
|
||||||
|
child: CupertinoUserInterfaceLevel(
|
||||||
|
data: CupertinoUserInterfaceLevelData.base,
|
||||||
|
child: CupertinoTheme(
|
||||||
|
data: effectiveThemeData,
|
||||||
|
child: HeroControllerScope(
|
||||||
|
controller: _heroController,
|
||||||
|
child: Builder(
|
||||||
|
builder: _buildWidgetApp,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
@ -205,6 +206,58 @@ class MaterialApp 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 [MaterialApp] that uses the [Router] instead of a [Navigator].
|
||||||
|
const MaterialApp.router({
|
||||||
|
Key key,
|
||||||
|
this.routeInformationProvider,
|
||||||
|
@required this.routeInformationParser,
|
||||||
|
@required this.routerDelegate,
|
||||||
|
this.backButtonDispatcher,
|
||||||
|
this.builder,
|
||||||
|
this.title = '',
|
||||||
|
this.onGenerateTitle,
|
||||||
|
this.color,
|
||||||
|
this.theme,
|
||||||
|
this.darkTheme,
|
||||||
|
this.highContrastTheme,
|
||||||
|
this.highContrastDarkTheme,
|
||||||
|
this.themeMode = ThemeMode.system,
|
||||||
|
this.locale,
|
||||||
|
this.localizationsDelegates,
|
||||||
|
this.localeListResolutionCallback,
|
||||||
|
this.localeResolutionCallback,
|
||||||
|
this.supportedLocales = const <Locale>[Locale('en', 'US')],
|
||||||
|
this.debugShowMaterialGrid = false,
|
||||||
|
this.showPerformanceOverlay = false,
|
||||||
|
this.checkerboardRasterCacheImages = false,
|
||||||
|
this.checkerboardOffscreenLayers = false,
|
||||||
|
this.showSemanticsDebugger = false,
|
||||||
|
this.debugShowCheckedModeBanner = true,
|
||||||
|
this.shortcuts,
|
||||||
|
this.actions,
|
||||||
|
}) : assert(routeInformationParser != null),
|
||||||
|
assert(routerDelegate != null),
|
||||||
|
assert(title != null),
|
||||||
|
assert(debugShowMaterialGrid != 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}
|
||||||
@ -238,6 +291,18 @@ class MaterialApp 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}
|
||||||
///
|
///
|
||||||
/// Material specific features such as [showDialog] and [showMenu], and widgets
|
/// Material specific features such as [showDialog] and [showMenu], and widgets
|
||||||
@ -611,6 +676,8 @@ class _MaterialScrollBehavior extends ScrollBehavior {
|
|||||||
class _MaterialAppState extends State<MaterialApp> {
|
class _MaterialAppState extends State<MaterialApp> {
|
||||||
HeroController _heroController;
|
HeroController _heroController;
|
||||||
|
|
||||||
|
bool get _usesRouter => widget.routerDelegate != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -629,24 +696,15 @@ class _MaterialAppState extends State<MaterialApp> {
|
|||||||
yield DefaultCupertinoLocalizations.delegate;
|
yield DefaultCupertinoLocalizations.delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
|
||||||
Widget build(BuildContext context) {
|
return FloatingActionButton(
|
||||||
Widget result = HeroControllerScope(
|
child: const Icon(Icons.search),
|
||||||
controller: _heroController,
|
onPressed: onPressed,
|
||||||
child: WidgetsApp(
|
mini: true,
|
||||||
key: GlobalObjectKey(this),
|
);
|
||||||
navigatorKey: widget.navigatorKey,
|
}
|
||||||
navigatorObservers: widget.navigatorObservers,
|
|
||||||
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
|
Widget _materialBuilder(BuildContext context, Widget child) {
|
||||||
return MaterialPageRoute<T>(settings: settings, builder: builder);
|
|
||||||
},
|
|
||||||
home: widget.home,
|
|
||||||
routes: widget.routes,
|
|
||||||
initialRoute: widget.initialRoute,
|
|
||||||
onGenerateRoute: widget.onGenerateRoute,
|
|
||||||
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
|
|
||||||
onUnknownRoute: widget.onUnknownRoute,
|
|
||||||
builder: (BuildContext context, Widget child) {
|
|
||||||
// Resolve which theme to use based on brightness and high contrast.
|
// Resolve which theme to use based on brightness and high contrast.
|
||||||
final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
|
final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
|
||||||
final Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);
|
final Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);
|
||||||
@ -686,18 +744,29 @@ class _MaterialAppState extends State<MaterialApp> {
|
|||||||
)
|
)
|
||||||
: child,
|
: child,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
title: widget.title,
|
|
||||||
onGenerateTitle: widget.onGenerateTitle,
|
Widget _buildWidgetApp(BuildContext context) {
|
||||||
textStyle: _errorTextStyle,
|
|
||||||
// The color property is always pulled from the light theme, even if dark
|
// The color property is always pulled from the light theme, even if dark
|
||||||
// mode is activated. This was done to simplify the technical details
|
// mode is activated. This was done to simplify the technical details
|
||||||
// of switching themes and it was deemed acceptable because this color
|
// of switching themes and it was deemed acceptable because this color
|
||||||
// property is only used on old Android OSes to color the app bar in
|
// property is only used on old Android OSes to color the app bar in
|
||||||
// Android's switcher UI.
|
// Android's switcher UI.
|
||||||
//
|
//
|
||||||
// blue is the primary color of the default theme
|
// blue is the primary color of the default theme.
|
||||||
color: widget.color ?? widget.theme?.primaryColor ?? Colors.blue,
|
final Color materialColor = widget.color ?? widget.theme?.primaryColor ?? Colors.blue;
|
||||||
|
if (_usesRouter) {
|
||||||
|
return WidgetsApp.router(
|
||||||
|
key: GlobalObjectKey(this),
|
||||||
|
routeInformationProvider: widget.routeInformationProvider,
|
||||||
|
routeInformationParser: widget.routeInformationParser,
|
||||||
|
routerDelegate: widget.routerDelegate,
|
||||||
|
backButtonDispatcher: widget.backButtonDispatcher,
|
||||||
|
builder: _materialBuilder,
|
||||||
|
title: widget.title,
|
||||||
|
onGenerateTitle: widget.onGenerateTitle,
|
||||||
|
textStyle: _errorTextStyle,
|
||||||
|
color: materialColor,
|
||||||
locale: widget.locale,
|
locale: widget.locale,
|
||||||
localizationsDelegates: _localizationsDelegates,
|
localizationsDelegates: _localizationsDelegates,
|
||||||
localeResolutionCallback: widget.localeResolutionCallback,
|
localeResolutionCallback: widget.localeResolutionCallback,
|
||||||
@ -708,17 +777,49 @@ class _MaterialAppState extends State<MaterialApp> {
|
|||||||
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
|
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
|
||||||
showSemanticsDebugger: widget.showSemanticsDebugger,
|
showSemanticsDebugger: widget.showSemanticsDebugger,
|
||||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||||
inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) {
|
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
|
||||||
return FloatingActionButton(
|
|
||||||
child: const Icon(Icons.search),
|
|
||||||
onPressed: onPressed,
|
|
||||||
mini: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
shortcuts: widget.shortcuts,
|
shortcuts: widget.shortcuts,
|
||||||
actions: widget.actions,
|
actions: widget.actions,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WidgetsApp(
|
||||||
|
key: GlobalObjectKey(this),
|
||||||
|
navigatorKey: widget.navigatorKey,
|
||||||
|
navigatorObservers: widget.navigatorObservers,
|
||||||
|
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
|
||||||
|
return MaterialPageRoute<T>(settings: settings, builder: builder);
|
||||||
|
},
|
||||||
|
home: widget.home,
|
||||||
|
routes: widget.routes,
|
||||||
|
initialRoute: widget.initialRoute,
|
||||||
|
onGenerateRoute: widget.onGenerateRoute,
|
||||||
|
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
|
||||||
|
onUnknownRoute: widget.onUnknownRoute,
|
||||||
|
builder: _materialBuilder,
|
||||||
|
title: widget.title,
|
||||||
|
onGenerateTitle: widget.onGenerateTitle,
|
||||||
|
textStyle: _errorTextStyle,
|
||||||
|
color: materialColor,
|
||||||
|
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
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget result = _buildWidgetApp(context);
|
||||||
|
|
||||||
assert(() {
|
assert(() {
|
||||||
if (widget.debugShowMaterialGrid) {
|
if (widget.debugShowMaterialGrid) {
|
||||||
@ -735,7 +836,10 @@ class _MaterialAppState extends State<MaterialApp> {
|
|||||||
|
|
||||||
return ScrollConfiguration(
|
return ScrollConfiguration(
|
||||||
behavior: _MaterialScrollBehavior(),
|
behavior: _MaterialScrollBehavior(),
|
||||||
|
child: HeroControllerScope(
|
||||||
|
controller: _heroController,
|
||||||
child: result,
|
child: result,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
/// * `routeInformationUpdated`, which is called by the [Router] when the
|
||||||
/// such as time picker is closed.)
|
/// application navigate to a new location.
|
||||||
///
|
|
||||||
/// * `routeReplaced`, which is called when a route is replaced.
|
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
@ -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 {
|
|||||||
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import 'media_query.dart';
|
|||||||
import 'navigator.dart';
|
import 'navigator.dart';
|
||||||
import 'pages.dart';
|
import 'pages.dart';
|
||||||
import 'performance_overlay.dart';
|
import 'performance_overlay.dart';
|
||||||
|
import 'router.dart';
|
||||||
import 'scrollable.dart';
|
import 'scrollable.dart';
|
||||||
import 'semantics_debugger.dart';
|
import 'semantics_debugger.dart';
|
||||||
import 'shortcuts.dart';
|
import 'shortcuts.dart';
|
||||||
@ -260,6 +261,62 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
assert(showSemanticsDebugger != null),
|
assert(showSemanticsDebugger != null),
|
||||||
assert(debugShowCheckedModeBanner != null),
|
assert(debugShowCheckedModeBanner != null),
|
||||||
assert(debugShowWidgetInspector != null),
|
assert(debugShowWidgetInspector != null),
|
||||||
|
routeInformationProvider = null,
|
||||||
|
routeInformationParser = null,
|
||||||
|
routerDelegate = null,
|
||||||
|
backButtonDispatcher = null,
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
/// Creates a [WidgetsApp] that uses the [Router] instead of a [Navigator].
|
||||||
|
WidgetsApp.router({
|
||||||
|
Key key,
|
||||||
|
this.routeInformationProvider,
|
||||||
|
@required this.routeInformationParser,
|
||||||
|
@required this.routerDelegate,
|
||||||
|
BackButtonDispatcher backButtonDispatcher,
|
||||||
|
this.builder,
|
||||||
|
this.title = '',
|
||||||
|
this.onGenerateTitle,
|
||||||
|
this.textStyle,
|
||||||
|
@required 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.debugShowWidgetInspector = false,
|
||||||
|
this.debugShowCheckedModeBanner = true,
|
||||||
|
this.inspectorSelectButtonBuilder,
|
||||||
|
this.shortcuts,
|
||||||
|
this.actions,
|
||||||
|
}) : assert(
|
||||||
|
routeInformationParser != null &&
|
||||||
|
routerDelegate != null,
|
||||||
|
'The routeInformationParser and routerDelegate cannot be null.'
|
||||||
|
),
|
||||||
|
assert(title != null),
|
||||||
|
assert(color != null),
|
||||||
|
assert(supportedLocales != null && supportedLocales.isNotEmpty),
|
||||||
|
assert(showPerformanceOverlay != null),
|
||||||
|
assert(checkerboardRasterCacheImages != null),
|
||||||
|
assert(checkerboardOffscreenLayers != null),
|
||||||
|
assert(showSemanticsDebugger != null),
|
||||||
|
assert(debugShowCheckedModeBanner != null),
|
||||||
|
assert(debugShowWidgetInspector != null),
|
||||||
|
navigatorObservers = null,
|
||||||
|
backButtonDispatcher = backButtonDispatcher ?? RootBackButtonDispatcher(),
|
||||||
|
navigatorKey = null,
|
||||||
|
onGenerateRoute = null,
|
||||||
|
pageRouteBuilder = null,
|
||||||
|
home = null,
|
||||||
|
onGenerateInitialRoutes = null,
|
||||||
|
onUnknownRoute = null,
|
||||||
|
routes = null,
|
||||||
|
initialRoute = null,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// {@template flutter.widgets.widgetsApp.navigatorKey}
|
/// {@template flutter.widgets.widgetsApp.navigatorKey}
|
||||||
@ -321,6 +378,71 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
/// or a [CupertinoPageRoute] should be used for building page transitions.
|
/// or a [CupertinoPageRoute] should be used for building page transitions.
|
||||||
final PageRouteFactory pageRouteBuilder;
|
final PageRouteFactory pageRouteBuilder;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.widgetsApp.routeInformationParser}
|
||||||
|
/// A delegate to parse the route information from the
|
||||||
|
/// [routeInformationProvider] into a generic data type to be processed by
|
||||||
|
/// the [routerDelegate] at a later stage.
|
||||||
|
///
|
||||||
|
/// This object will be used by the underlying [Router].
|
||||||
|
///
|
||||||
|
/// The generic type `T` must match the generic type of the [routerDelegate].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Router.routeInformationParser]: which receives this object when this
|
||||||
|
/// widget builds the [Router].
|
||||||
|
/// {@endtemplate}
|
||||||
|
final RouteInformationParser<Object> routeInformationParser;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.widgetsApp.routerDelegate}
|
||||||
|
/// A delegate that configures a widget, typically a [Navigator], with
|
||||||
|
/// parsed result from the [routeInformationParser].
|
||||||
|
///
|
||||||
|
/// This object will be used by the underlying [Router].
|
||||||
|
///
|
||||||
|
/// The generic type `T` must match the generic type of the
|
||||||
|
/// [routeInformationParser].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Router.routerDelegate]: which receives this object when this widget
|
||||||
|
/// builds the [Router].
|
||||||
|
/// {@endtemplate}
|
||||||
|
final RouterDelegate<Object> routerDelegate;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.widgetsApp.backButtonDispatcher}
|
||||||
|
/// A delegate that decide whether to handle the Android back button intent.
|
||||||
|
///
|
||||||
|
/// This object will be used by the underlying [Router].
|
||||||
|
///
|
||||||
|
/// If this is not provided, the widgets app will create a
|
||||||
|
/// [RootBackButtonDispatcher] by default.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Router.backButtonDispatcher]: which receives this object when this
|
||||||
|
/// widget builds the [Router].
|
||||||
|
/// {@endtemplate}
|
||||||
|
final BackButtonDispatcher backButtonDispatcher;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.widgetsApp.routeInformationProvider}
|
||||||
|
/// A object that provides route information through the
|
||||||
|
/// [RouteInformationProvider.value] and notifies its listener when its value
|
||||||
|
/// changes.
|
||||||
|
///
|
||||||
|
/// This object will be used by the underlying [Router].
|
||||||
|
///
|
||||||
|
/// If this is not provided, the widgets app will create a
|
||||||
|
/// [PlatformRouteInformationProvider] with initial route name equals to
|
||||||
|
/// the [Window.defaultRouteName] by default.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Router.routeInformationProvider]: which receives this object when this
|
||||||
|
/// widget builds the [Router].
|
||||||
|
/// {@endtemplate}
|
||||||
|
final RouteInformationProvider routeInformationProvider;
|
||||||
|
|
||||||
/// {@template flutter.widgets.widgetsApp.home}
|
/// {@template flutter.widgets.widgetsApp.home}
|
||||||
/// The widget for the default route of the app ([Navigator.defaultRouteName],
|
/// The widget for the default route of the app ([Navigator.defaultRouteName],
|
||||||
/// which is `/`).
|
/// which is `/`).
|
||||||
@ -960,10 +1082,21 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
||||||
// STATE LIFECYCLE
|
// STATE LIFECYCLE
|
||||||
|
|
||||||
|
// If window.defaultRouteName isn't '/', we should assume it was set
|
||||||
|
// intentionally via `setInitialRoute`, and should override whatever is in
|
||||||
|
// [widget.initialRoute].
|
||||||
|
String get _initialRouteName => WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName
|
||||||
|
? WidgetsBinding.instance.window.defaultRouteName
|
||||||
|
: widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
if (_usesRouter) {
|
||||||
|
_updateRouter();
|
||||||
|
} else {
|
||||||
_updateNavigator();
|
_updateNavigator();
|
||||||
|
}
|
||||||
_locale = _resolveLocales(WidgetsBinding.instance.window.locales, widget.supportedLocales);
|
_locale = _resolveLocales(WidgetsBinding.instance.window.locales, widget.supportedLocales);
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
}
|
}
|
||||||
@ -971,16 +1104,37 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||||||
@override
|
@override
|
||||||
void didUpdateWidget(WidgetsApp oldWidget) {
|
void didUpdateWidget(WidgetsApp oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.navigatorKey != oldWidget.navigatorKey)
|
if (oldWidget.routeInformationProvider != widget.routeInformationProvider) {
|
||||||
|
_updateRouter();
|
||||||
|
}
|
||||||
|
if (widget.navigatorKey != oldWidget.navigatorKey) {
|
||||||
_updateNavigator();
|
_updateNavigator();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_defaultRouteInformationProvider?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _usesRouter => widget.routerDelegate != null;
|
||||||
|
|
||||||
|
// ROUTER
|
||||||
|
RouteInformationProvider get _effectiveRouteInformationProvider => widget.routeInformationProvider ?? _defaultRouteInformationProvider;
|
||||||
|
PlatformRouteInformationProvider _defaultRouteInformationProvider;
|
||||||
|
|
||||||
|
void _updateRouter() {
|
||||||
|
_defaultRouteInformationProvider?.dispose();
|
||||||
|
if (widget.routeInformationProvider == null)
|
||||||
|
_defaultRouteInformationProvider = PlatformRouteInformationProvider(
|
||||||
|
initialRouteInformation: RouteInformation(
|
||||||
|
location: _initialRouteName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// NAVIGATOR
|
// NAVIGATOR
|
||||||
|
|
||||||
GlobalKey<NavigatorState> _navigator;
|
GlobalKey<NavigatorState> _navigator;
|
||||||
@ -1050,6 +1204,11 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||||||
@override
|
@override
|
||||||
Future<bool> didPopRoute() async {
|
Future<bool> didPopRoute() async {
|
||||||
assert(mounted);
|
assert(mounted);
|
||||||
|
// The back button dispatcher should handle the pop route if we use a
|
||||||
|
// router.
|
||||||
|
if (_usesRouter)
|
||||||
|
return false;
|
||||||
|
|
||||||
final NavigatorState navigator = _navigator?.currentState;
|
final NavigatorState navigator = _navigator?.currentState;
|
||||||
if (navigator == null)
|
if (navigator == null)
|
||||||
return false;
|
return false;
|
||||||
@ -1059,6 +1218,11 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||||||
@override
|
@override
|
||||||
Future<bool> didPushRoute(String route) async {
|
Future<bool> didPushRoute(String route) async {
|
||||||
assert(mounted);
|
assert(mounted);
|
||||||
|
// The route name provider should handle the push route if we uses a
|
||||||
|
// router.
|
||||||
|
if (_usesRouter)
|
||||||
|
return false;
|
||||||
|
|
||||||
final NavigatorState navigator = _navigator?.currentState;
|
final NavigatorState navigator = _navigator?.currentState;
|
||||||
if (navigator == null)
|
if (navigator == null)
|
||||||
return false;
|
return false;
|
||||||
@ -1291,16 +1455,20 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget navigator;
|
Widget routing;
|
||||||
if (_navigator != null) {
|
if (_usesRouter) {
|
||||||
navigator = Navigator(
|
assert(_effectiveRouteInformationProvider != null);
|
||||||
|
routing = Router<dynamic>(
|
||||||
|
routeInformationProvider: _effectiveRouteInformationProvider,
|
||||||
|
routeInformationParser: widget.routeInformationParser,
|
||||||
|
routerDelegate: widget.routerDelegate,
|
||||||
|
backButtonDispatcher: widget.backButtonDispatcher,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert(_navigator != null);
|
||||||
|
routing = Navigator(
|
||||||
key: _navigator,
|
key: _navigator,
|
||||||
// If window.defaultRouteName isn't '/', we should assume it was set
|
initialRoute: _initialRouteName,
|
||||||
// intentionally via `setInitialRoute`, and should override whatever
|
|
||||||
// is in [widget.initialRoute].
|
|
||||||
initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName
|
|
||||||
? WidgetsBinding.instance.window.defaultRouteName
|
|
||||||
: widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
|
|
||||||
onGenerateRoute: _onGenerateRoute,
|
onGenerateRoute: _onGenerateRoute,
|
||||||
onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
|
onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
|
||||||
? Navigator.defaultGenerateInitialRoutes
|
? Navigator.defaultGenerateInitialRoutes
|
||||||
@ -1317,12 +1485,12 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||||||
if (widget.builder != null) {
|
if (widget.builder != null) {
|
||||||
result = Builder(
|
result = Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return widget.builder(context, navigator);
|
return widget.builder(context, routing);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
assert(navigator != null);
|
assert(routing != null);
|
||||||
result = navigator;
|
result = routing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.textStyle != null) {
|
if (widget.textStyle != null) {
|
||||||
|
@ -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 {
|
|||||||
/// [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 {
|
|||||||
/// [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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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';
|
|||||||
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 {
|
|||||||
_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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
// 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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
1248
packages/flutter/lib/src/widgets/router.dart
Normal file
1248
packages/flutter/lib/src/widgets/router.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 @@
|
|||||||
|
|
||||||
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() {
|
|||||||
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 @@
|
|||||||
|
|
||||||
// @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() {
|
|||||||
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 {
|
|||||||
@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() {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
705
packages/flutter/test/widgets/router_test.dart
Normal file
705
packages/flutter/test/widgets/router_test.dart
Normal file
@ -0,0 +1,705 @@
|
|||||||
|
// 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_test/flutter_test.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Simple router basic functionality - synchronized', (WidgetTester tester) async {
|
||||||
|
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'initial',
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(buildBoilerPlate(
|
||||||
|
Router<RouteInformation>(
|
||||||
|
routeInformationProvider: provider,
|
||||||
|
routeInformationParser: SimpleRouteInformationParser(),
|
||||||
|
routerDelegate: SimpleRouterDelegate(
|
||||||
|
builder: (BuildContext context, RouteInformation information) {
|
||||||
|
return Text(information.location);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
expect(find.text('initial'), findsOneWidget);
|
||||||
|
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'update',
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('initial'), findsNothing);
|
||||||
|
expect(find.text('update'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Simple router basic functionality - asynchronized', (WidgetTester tester) async {
|
||||||
|
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'initial',
|
||||||
|
);
|
||||||
|
final SimpleAsyncRouteInformationParser parser = SimpleAsyncRouteInformationParser();
|
||||||
|
final SimpleAsyncRouterDelegate delegate = SimpleAsyncRouterDelegate(
|
||||||
|
builder: (BuildContext context, RouteInformation information) {
|
||||||
|
if (information == null)
|
||||||
|
return const Text('waiting');
|
||||||
|
return Text(information.location);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
await tester.pumpWidget(buildBoilerPlate(
|
||||||
|
Router<RouteInformation>(
|
||||||
|
routeInformationProvider: provider,
|
||||||
|
routeInformationParser: parser,
|
||||||
|
routerDelegate: delegate,
|
||||||
|
)
|
||||||
|
));
|
||||||
|
// Future has not yet completed.
|
||||||
|
expect(find.text('waiting'), findsOneWidget);
|
||||||
|
|
||||||
|
await parser.parsingFuture;
|
||||||
|
await delegate.setNewRouteFuture;
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('initial'), findsOneWidget);
|
||||||
|
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'update',
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
// Future has not yet completed.
|
||||||
|
expect(find.text('initial'), findsOneWidget);
|
||||||
|
|
||||||
|
await parser.parsingFuture;
|
||||||
|
await delegate.setNewRouteFuture;
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('update'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Simple router can handle pop route', (WidgetTester tester) async {
|
||||||
|
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'initial',
|
||||||
|
);
|
||||||
|
final BackButtonDispatcher dispatcher = RootBackButtonDispatcher();
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildBoilerPlate(
|
||||||
|
Router<RouteInformation>(
|
||||||
|
routeInformationProvider: provider,
|
||||||
|
routeInformationParser: SimpleRouteInformationParser(),
|
||||||
|
routerDelegate: SimpleRouterDelegate(
|
||||||
|
builder: (BuildContext context, RouteInformation information) {
|
||||||
|
return Text(information.location);
|
||||||
|
},
|
||||||
|
onPopRoute: () {
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'popped',
|
||||||
|
);
|
||||||
|
return SynchronousFuture<bool>(true);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
backButtonDispatcher: dispatcher,
|
||||||
|
)
|
||||||
|
));
|
||||||
|
expect(find.text('initial'), findsOneWidget);
|
||||||
|
|
||||||
|
bool result = false;
|
||||||
|
// SynchronousFuture should complete immediately.
|
||||||
|
dispatcher.invokeCallback(SynchronousFuture<bool>(false))
|
||||||
|
.then((bool data) {
|
||||||
|
result = data;
|
||||||
|
});
|
||||||
|
expect(result, isTrue);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('popped'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('PopNavigatorRouterDelegateMixin works', (WidgetTester tester) async {
|
||||||
|
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'initial',
|
||||||
|
);
|
||||||
|
final BackButtonDispatcher dispatcher = RootBackButtonDispatcher();
|
||||||
|
final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate(
|
||||||
|
builder: (BuildContext context, RouteInformation information) {
|
||||||
|
return Text(information.location);
|
||||||
|
},
|
||||||
|
onPopPage: (Route<void> route, void result) {
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'popped',
|
||||||
|
);
|
||||||
|
return route.didPop(result);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(buildBoilerPlate(
|
||||||
|
Router<RouteInformation>(
|
||||||
|
routeInformationProvider: provider,
|
||||||
|
routeInformationParser: SimpleRouteInformationParser(),
|
||||||
|
routerDelegate: delegate,
|
||||||
|
backButtonDispatcher: dispatcher,
|
||||||
|
)
|
||||||
|
));
|
||||||
|
expect(find.text('initial'), findsOneWidget);
|
||||||
|
|
||||||
|
// Pushes a nameless route.
|
||||||
|
showDialog<void>(
|
||||||
|
useRootNavigator: false,
|
||||||
|
context: delegate.navigatorKey.currentContext,
|
||||||
|
builder: (BuildContext context) => const Text('dialog')
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('dialog'), findsOneWidget);
|
||||||
|
|
||||||
|
// Pops the nameless route and makes sure the initial page is shown.
|
||||||
|
bool result = false;
|
||||||
|
result = await dispatcher.invokeCallback(SynchronousFuture<bool>(false));
|
||||||
|
expect(result, isTrue);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('initial'), findsOneWidget);
|
||||||
|
expect(find.text('dialog'), findsNothing);
|
||||||
|
|
||||||
|
// Pops one more time.
|
||||||
|
result = false;
|
||||||
|
result = await dispatcher.invokeCallback(SynchronousFuture<bool>(false));
|
||||||
|
expect(result, isTrue);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('popped'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Nested routers back button dispatcher works', (WidgetTester tester) async {
|
||||||
|
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'initial',
|
||||||
|
);
|
||||||
|
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
|
||||||
|
await tester.pumpWidget(buildBoilerPlate(
|
||||||
|
Router<RouteInformation>(
|
||||||
|
backButtonDispatcher: outerDispatcher,
|
||||||
|
routeInformationProvider: provider,
|
||||||
|
routeInformationParser: SimpleRouteInformationParser(),
|
||||||
|
routerDelegate: SimpleRouterDelegate(
|
||||||
|
builder: (BuildContext context, RouteInformation information) {
|
||||||
|
final BackButtonDispatcher innerDispatcher = ChildBackButtonDispatcher(outerDispatcher);
|
||||||
|
innerDispatcher.takePriority();
|
||||||
|
// Creates the sub-router.
|
||||||
|
return Router<RouteInformation>(
|
||||||
|
backButtonDispatcher: innerDispatcher,
|
||||||
|
routerDelegate: SimpleRouterDelegate(
|
||||||
|
builder: (BuildContext context, RouteInformation innerInformation) {
|
||||||
|
return Text(information.location);
|
||||||
|
},
|
||||||
|
onPopRoute: () {
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'popped inner',
|
||||||
|
);
|
||||||
|
return SynchronousFuture<bool>(true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onPopRoute: () {
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'popped outter',
|
||||||
|
);
|
||||||
|
return SynchronousFuture<bool>(true);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
expect(find.text('initial'), findsOneWidget);
|
||||||
|
|
||||||
|
// The outer dispatcher should trigger the pop on the inner router.
|
||||||
|
bool result = false;
|
||||||
|
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
|
||||||
|
expect(result, isTrue);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('popped inner'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Nested router back button dispatcher works for multiple children', (WidgetTester tester) async {
|
||||||
|
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'initial',
|
||||||
|
);
|
||||||
|
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
|
||||||
|
final BackButtonDispatcher innerDispatcher1 = ChildBackButtonDispatcher(outerDispatcher);
|
||||||
|
final BackButtonDispatcher innerDispatcher2 = ChildBackButtonDispatcher(outerDispatcher);
|
||||||
|
await tester.pumpWidget(buildBoilerPlate(
|
||||||
|
Router<RouteInformation>(
|
||||||
|
backButtonDispatcher: outerDispatcher,
|
||||||
|
routeInformationProvider: provider,
|
||||||
|
routeInformationParser: SimpleRouteInformationParser(),
|
||||||
|
routerDelegate: SimpleRouterDelegate(
|
||||||
|
builder: (BuildContext context, RouteInformation information) {
|
||||||
|
// Creates the sub-router.
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(information.location),
|
||||||
|
Router<RouteInformation>(
|
||||||
|
backButtonDispatcher: innerDispatcher1,
|
||||||
|
routerDelegate: SimpleRouterDelegate(
|
||||||
|
builder: (BuildContext context, RouteInformation innerInformation) {
|
||||||
|
return Container();
|
||||||
|
},
|
||||||
|
onPopRoute: () {
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'popped inner1',
|
||||||
|
);
|
||||||
|
return SynchronousFuture<bool>(true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Router<RouteInformation>(
|
||||||
|
backButtonDispatcher: innerDispatcher2,
|
||||||
|
routerDelegate: SimpleRouterDelegate(
|
||||||
|
builder: (BuildContext context, RouteInformation innerInformation) {
|
||||||
|
return Container();
|
||||||
|
},
|
||||||
|
onPopRoute: () {
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'popped inner2',
|
||||||
|
);
|
||||||
|
return SynchronousFuture<bool>(true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onPopRoute: () {
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'popped outter',
|
||||||
|
);
|
||||||
|
return SynchronousFuture<bool>(true);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
expect(find.text('initial'), findsOneWidget);
|
||||||
|
|
||||||
|
// If none of the children have taken the priority, the root router handles
|
||||||
|
// the pop.
|
||||||
|
bool result = false;
|
||||||
|
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
|
||||||
|
expect(result, isTrue);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('popped outter'), findsOneWidget);
|
||||||
|
|
||||||
|
innerDispatcher1.takePriority();
|
||||||
|
result = false;
|
||||||
|
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
|
||||||
|
expect(result, isTrue);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('popped inner1'), findsOneWidget);
|
||||||
|
|
||||||
|
// The last child dispatcher that took priority handles the pop.
|
||||||
|
innerDispatcher2.takePriority();
|
||||||
|
result = false;
|
||||||
|
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
|
||||||
|
expect(result, isTrue);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('popped inner2'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('router does report URL change correctly', (WidgetTester tester) async {
|
||||||
|
RouteInformation reportedRouteInformation;
|
||||||
|
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(
|
||||||
|
onRouterReport: (RouteInformation information) {
|
||||||
|
// Makes sure we only report once after manually cleaning up.
|
||||||
|
expect(reportedRouteInformation, isNull);
|
||||||
|
reportedRouteInformation = information;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
final SimpleRouterDelegate delegate = SimpleRouterDelegate(
|
||||||
|
reportConfiguration: true,
|
||||||
|
builder: (BuildContext context, RouteInformation information) {
|
||||||
|
return Text(information.location);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
delegate.onPopRoute = () {
|
||||||
|
delegate.routeInformation = const RouteInformation(
|
||||||
|
location: 'popped',
|
||||||
|
);
|
||||||
|
return SynchronousFuture<bool>(true);
|
||||||
|
};
|
||||||
|
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
|
||||||
|
|
||||||
|
provider.value = const RouteInformation(
|
||||||
|
location: 'initial',
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildBoilerPlate(
|
||||||
|
Router<RouteInformation>(
|
||||||
|
backButtonDispatcher: outerDispatcher,
|
||||||
|
routeInformationProvider: provider,
|
||||||
|
routeInformationParser: SimpleRouteInformationParser(),
|
||||||
|
routerDelegate: delegate,
|
||||||
|
)
|
||||||
|
));
|
||||||
|
expect(find.text('initial'), findsOneWidget);
|
||||||
|
expect(reportedRouteInformation, isNull);
|
||||||
|
delegate.routeInformation = const RouteInformation(
|
||||||
|
location: 'update',
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('initial'), findsNothing);
|
||||||
|
expect(find.text('update'), findsOneWidget);
|
||||||
|
expect(reportedRouteInformation.location, 'update');
|
||||||
|
|
||||||
|
// The router should not report if only state changes.
|
||||||
|
reportedRouteInformation = null;
|
||||||
|
delegate.routeInformation = const RouteInformation(
|
||||||
|
location: 'update',
|
||||||
|
state: 'another state',
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('update'), findsOneWidget);
|
||||||
|
expect(reportedRouteInformation, isNull);
|
||||||
|
|
||||||
|
reportedRouteInformation = null;
|
||||||
|
bool result = false;
|
||||||
|
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
|
||||||
|
expect(result, isTrue);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('popped'), findsOneWidget);
|
||||||
|
expect(reportedRouteInformation.location, 'popped');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('router can be forced to recognize or ignore navigating events', (WidgetTester tester) async {
|
||||||
|
RouteInformation reportedRouteInformation;
|
||||||
|
bool isNavigating = false;
|
||||||
|
RouteInformation nextRouteInformation;
|
||||||
|
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(
|
||||||
|
onRouterReport: (RouteInformation information) {
|
||||||
|
// Makes sure we only report once after manually cleaning up.
|
||||||
|
expect(reportedRouteInformation, isNull);
|
||||||
|
reportedRouteInformation = 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: () {
|
||||||
|
if (isNavigating) {
|
||||||
|
Router.navigate(context, () {
|
||||||
|
if (delegate.routeInformation != nextRouteInformation)
|
||||||
|
delegate.routeInformation = nextRouteInformation;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
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(reportedRouteInformation, 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(reportedRouteInformation, isNull);
|
||||||
|
|
||||||
|
isNavigating = true;
|
||||||
|
// This should not trigger any real navigating event because the
|
||||||
|
// nextRouteInformation does not change. However, the router should still
|
||||||
|
// report a route information because isNavigating = true.
|
||||||
|
await tester.tap(find.byType(ElevatedButton));
|
||||||
|
await tester.pump();
|
||||||
|
expect(reportedRouteInformation.location, 'update');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('PlatformRouteInformationProvider works', (WidgetTester tester) async {
|
||||||
|
final RouteInformationProvider provider = PlatformRouteInformationProvider(
|
||||||
|
initialRouteInformation: const RouteInformation(
|
||||||
|
location: 'initial',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final SimpleRouterDelegate delegate = SimpleRouterDelegate(
|
||||||
|
builder: (BuildContext context, RouteInformation information) {
|
||||||
|
final List<Widget> children = <Widget>[];
|
||||||
|
if (information.location != null)
|
||||||
|
children.add(Text(information.location));
|
||||||
|
if (information.state != null)
|
||||||
|
children.add(Text(information.state.toString()));
|
||||||
|
return Column(
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp.router(
|
||||||
|
routeInformationProvider: provider,
|
||||||
|
routeInformationParser: SimpleRouteInformationParser(),
|
||||||
|
routerDelegate: delegate,
|
||||||
|
));
|
||||||
|
expect(find.text('initial'), findsOneWidget);
|
||||||
|
|
||||||
|
// Pushes through the `pushRouteInformation` in the navigation method channel.
|
||||||
|
const Map<String, dynamic> testRouteInformation = <String, dynamic>{
|
||||||
|
'location': 'testRouteName',
|
||||||
|
'state': 'state',
|
||||||
|
};
|
||||||
|
final ByteData routerMessage = const JSONMethodCodec().encodeMethodCall(
|
||||||
|
const MethodCall('pushRouteInformation', testRouteInformation)
|
||||||
|
);
|
||||||
|
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', routerMessage, (_) { });
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('testRouteName'), findsOneWidget);
|
||||||
|
expect(find.text('state'), findsOneWidget);
|
||||||
|
|
||||||
|
// Pushes through the `pushRoute` in the navigation method channel.
|
||||||
|
const String testRouteName = 'newTestRouteName';
|
||||||
|
final ByteData message = const JSONMethodCodec().encodeMethodCall(
|
||||||
|
const MethodCall('pushRoute', testRouteName));
|
||||||
|
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('newTestRouteName'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('RootBackButtonDispatcher works', (WidgetTester tester) async {
|
||||||
|
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
|
||||||
|
final RouteInformationProvider provider = PlatformRouteInformationProvider(
|
||||||
|
initialRouteInformation: const RouteInformation(
|
||||||
|
location: 'initial',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final SimpleRouterDelegate delegate = SimpleRouterDelegate(
|
||||||
|
reportConfiguration: true,
|
||||||
|
builder: (BuildContext context, RouteInformation information) {
|
||||||
|
return Text(information.location);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
delegate.onPopRoute = () {
|
||||||
|
delegate.routeInformation = const RouteInformation(
|
||||||
|
location: 'popped',
|
||||||
|
);
|
||||||
|
return SynchronousFuture<bool>(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp.router(
|
||||||
|
backButtonDispatcher: outerDispatcher,
|
||||||
|
routeInformationProvider: provider,
|
||||||
|
routeInformationParser: SimpleRouteInformationParser(),
|
||||||
|
routerDelegate: delegate,
|
||||||
|
));
|
||||||
|
expect(find.text('initial'), findsOneWidget);
|
||||||
|
|
||||||
|
// Pop route through the message channel.
|
||||||
|
final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute'));
|
||||||
|
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('popped'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBoilerPlate(Widget child) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
|
||||||
|
typedef SimpleRouterDelegatePopRoute = Future<bool> Function();
|
||||||
|
typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result);
|
||||||
|
typedef RouterReportRouterInformation = void Function(RouteInformation);
|
||||||
|
|
||||||
|
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({
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleRouteInformationProvider extends RouteInformationProvider with ChangeNotifier {
|
||||||
|
SimpleRouteInformationProvider({
|
||||||
|
this.onRouterReport
|
||||||
|
});
|
||||||
|
|
||||||
|
RouterReportRouterInformation onRouterReport;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RouteInformation get value => _value;
|
||||||
|
RouteInformation _value;
|
||||||
|
set value(RouteInformation newValue) {
|
||||||
|
_value = newValue;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void routerReportsNewRouteInformation(RouteInformation routeInformation) {
|
||||||
|
if (onRouterReport != null)
|
||||||
|
onRouterReport(routeInformation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleAsyncRouteInformationParser extends RouteInformationParser<RouteInformation> {
|
||||||
|
SimpleAsyncRouteInformationParser();
|
||||||
|
|
||||||
|
Future<RouteInformation> parsingFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
|
||||||
|
return parsingFuture = Future<RouteInformation>.value(information);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RouteInformation restoreRouteInformation(RouteInformation configuration) {
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleAsyncRouterDelegate extends RouterDelegate<RouteInformation> with ChangeNotifier{
|
||||||
|
SimpleAsyncRouterDelegate({
|
||||||
|
@required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
RouteInformation get routeInformation => _routeInformation;
|
||||||
|
RouteInformation _routeInformation;
|
||||||
|
set routeInformation(RouteInformation newValue) {
|
||||||
|
_routeInformation = newValue;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleRouterDelegateBuilder builder;
|
||||||
|
Future<void> setNewRouteFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setNewRoutePath(RouteInformation configuration) {
|
||||||
|
_routeInformation = configuration;
|
||||||
|
return setNewRouteFuture = Future<void>.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> popRoute() {
|
||||||
|
return Future<bool>.value(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => builder(context, routeInformation);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user