Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
f9fd71bc
Unverified
Commit
f9fd71bc
authored
Aug 08, 2020
by
chunhtai
Committed by
GitHub
Aug 08, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement Router widget and widgets app api (#60299)
parent
12b8d9db
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
2997 additions
and
187 deletions
+2997
-187
app.dart
packages/flutter/lib/src/cupertino/app.dart
+139
-47
app.dart
packages/flutter/lib/src/material/app.dart
+178
-74
system_channels.dart
packages/flutter/lib/src/services/system_channels.dart
+9
-7
system_navigator.dart
packages/flutter/lib/src/services/system_navigator.dart
+33
-0
app.dart
packages/flutter/lib/src/widgets/app.dart
+182
-14
binding.dart
packages/flutter/lib/src/widgets/binding.dart
+34
-1
navigator.dart
packages/flutter/lib/src/widgets/navigator.dart
+4
-3
route_notification_messages.dart
.../flutter/lib/src/widgets/route_notification_messages.dart
+0
-41
router.dart
packages/flutter/lib/src/widgets/router.dart
+1248
-0
widgets.dart
packages/flutter/lib/widgets.dart
+1
-0
app_test.dart
packages/flutter/test/cupertino/app_test.dart
+99
-0
app_test.dart
packages/flutter/test/material/app_test.dart
+99
-0
app_test.dart
packages/flutter/test/widgets/app_test.dart
+112
-0
binding_test.dart
packages/flutter/test/widgets/binding_test.dart
+42
-0
route_notification_messages_test.dart
...lutter/test/widgets/route_notification_messages_test.dart
+112
-0
router_test.dart
packages/flutter/test/widgets/router_test.dart
+705
-0
No files found.
packages/flutter/lib/src/cupertino/app.dart
View file @
f9fd71bc
...
...
@@ -104,6 +104,50 @@ class CupertinoApp extends StatefulWidget {
assert
(
checkerboardOffscreenLayers
!=
null
),
assert
(
showSemanticsDebugger
!=
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
);
/// {@macro flutter.widgets.widgetsApp.navigatorKey}
...
...
@@ -143,6 +187,18 @@ class CupertinoApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.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}
final
TransitionBuilder
builder
;
...
...
@@ -286,6 +342,7 @@ class _AlwaysCupertinoScrollBehavior extends ScrollBehavior {
class
_CupertinoAppState
extends
State
<
CupertinoApp
>
{
HeroController
_heroController
;
bool
get
_usesRouter
=>
widget
.
routerDelegate
!=
null
;
@override
void
initState
()
{
...
...
@@ -304,6 +361,83 @@ class _CupertinoAppState extends State<CupertinoApp> {
yield
DefaultCupertinoLocalizations
.
delegate
;
}
Widget
_inspectorSelectButtonBuilder
(
BuildContext
context
,
VoidCallback
onPressed
)
{
return
CupertinoButton
.
filled
(
child:
const
Icon
(
CupertinoIcons
.
search
,
size:
28.0
,
color:
CupertinoColors
.
white
,
),
padding:
EdgeInsets
.
zero
,
onPressed:
onPressed
,
);
}
WidgetsApp
_buildWidgetApp
(
BuildContext
context
)
{
final
CupertinoThemeData
effectiveThemeData
=
CupertinoTheme
.
of
(
context
);
final
Color
color
=
CupertinoDynamicColor
.
resolve
(
widget
.
color
??
effectiveThemeData
.
primaryColor
,
context
);
if
(
_usesRouter
)
{
return
WidgetsApp
.
router
(
key:
GlobalObjectKey
(
this
),
routeInformationProvider:
widget
.
routeInformationProvider
,
routeInformationParser:
widget
.
routeInformationParser
,
routerDelegate:
widget
.
routerDelegate
,
backButtonDispatcher:
widget
.
backButtonDispatcher
,
builder:
widget
.
builder
,
title:
widget
.
title
,
onGenerateTitle:
widget
.
onGenerateTitle
,
textStyle:
effectiveThemeData
.
textTheme
.
textStyle
,
color:
color
,
locale:
widget
.
locale
,
localizationsDelegates:
_localizationsDelegates
,
localeResolutionCallback:
widget
.
localeResolutionCallback
,
localeListResolutionCallback:
widget
.
localeListResolutionCallback
,
supportedLocales:
widget
.
supportedLocales
,
showPerformanceOverlay:
widget
.
showPerformanceOverlay
,
checkerboardRasterCacheImages:
widget
.
checkerboardRasterCacheImages
,
checkerboardOffscreenLayers:
widget
.
checkerboardOffscreenLayers
,
showSemanticsDebugger:
widget
.
showSemanticsDebugger
,
debugShowCheckedModeBanner:
widget
.
debugShowCheckedModeBanner
,
inspectorSelectButtonBuilder:
_inspectorSelectButtonBuilder
,
shortcuts:
widget
.
shortcuts
,
actions:
widget
.
actions
,
);
}
return
WidgetsApp
(
key:
GlobalObjectKey
(
this
),
navigatorKey:
widget
.
navigatorKey
,
navigatorObservers:
widget
.
navigatorObservers
,
pageRouteBuilder:
<
T
>(
RouteSettings
settings
,
WidgetBuilder
builder
)
{
return
CupertinoPageRoute
<
T
>(
settings:
settings
,
builder:
builder
);
},
home:
widget
.
home
,
routes:
widget
.
routes
,
initialRoute:
widget
.
initialRoute
,
onGenerateRoute:
widget
.
onGenerateRoute
,
onGenerateInitialRoutes:
widget
.
onGenerateInitialRoutes
,
onUnknownRoute:
widget
.
onUnknownRoute
,
builder:
widget
.
builder
,
title:
widget
.
title
,
onGenerateTitle:
widget
.
onGenerateTitle
,
textStyle:
effectiveThemeData
.
textTheme
.
textStyle
,
color:
color
,
locale:
widget
.
locale
,
localizationsDelegates:
_localizationsDelegates
,
localeResolutionCallback:
widget
.
localeResolutionCallback
,
localeListResolutionCallback:
widget
.
localeListResolutionCallback
,
supportedLocales:
widget
.
supportedLocales
,
showPerformanceOverlay:
widget
.
showPerformanceOverlay
,
checkerboardRasterCacheImages:
widget
.
checkerboardRasterCacheImages
,
checkerboardOffscreenLayers:
widget
.
checkerboardOffscreenLayers
,
showSemanticsDebugger:
widget
.
showSemanticsDebugger
,
debugShowCheckedModeBanner:
widget
.
debugShowCheckedModeBanner
,
inspectorSelectButtonBuilder:
_inspectorSelectButtonBuilder
,
shortcuts:
widget
.
shortcuts
,
actions:
widget
.
actions
,
);
}
@override
Widget
build
(
BuildContext
context
)
{
final
CupertinoThemeData
effectiveThemeData
=
widget
.
theme
??
const
CupertinoThemeData
();
...
...
@@ -314,53 +448,11 @@ class _CupertinoAppState extends State<CupertinoApp> {
data:
CupertinoUserInterfaceLevelData
.
base
,
child:
CupertinoTheme
(
data:
effectiveThemeData
,
child:
Builder
(
builder:
(
BuildContext
context
)
{
return
HeroControllerScope
(
controller:
_heroController
,
child:
WidgetsApp
(
key:
GlobalObjectKey
(
this
),
navigatorKey:
widget
.
navigatorKey
,
navigatorObservers:
widget
.
navigatorObservers
,
pageRouteBuilder:
<
T
>(
RouteSettings
settings
,
WidgetBuilder
builder
)
=>
CupertinoPageRoute
<
T
>(
settings:
settings
,
builder:
builder
),
home:
widget
.
home
,
routes:
widget
.
routes
,
initialRoute:
widget
.
initialRoute
,
onGenerateRoute:
widget
.
onGenerateRoute
,
onGenerateInitialRoutes:
widget
.
onGenerateInitialRoutes
,
onUnknownRoute:
widget
.
onUnknownRoute
,
builder:
widget
.
builder
,
title:
widget
.
title
,
onGenerateTitle:
widget
.
onGenerateTitle
,
textStyle:
CupertinoTheme
.
of
(
context
).
textTheme
.
textStyle
,
color:
CupertinoDynamicColor
.
resolve
(
widget
.
color
??
effectiveThemeData
.
primaryColor
,
context
),
locale:
widget
.
locale
,
localizationsDelegates:
_localizationsDelegates
,
localeResolutionCallback:
widget
.
localeResolutionCallback
,
localeListResolutionCallback:
widget
.
localeListResolutionCallback
,
supportedLocales:
widget
.
supportedLocales
,
showPerformanceOverlay:
widget
.
showPerformanceOverlay
,
checkerboardRasterCacheImages:
widget
.
checkerboardRasterCacheImages
,
checkerboardOffscreenLayers:
widget
.
checkerboardOffscreenLayers
,
showSemanticsDebugger:
widget
.
showSemanticsDebugger
,
debugShowCheckedModeBanner:
widget
.
debugShowCheckedModeBanner
,
inspectorSelectButtonBuilder:
(
BuildContext
context
,
VoidCallback
onPressed
)
{
return
CupertinoButton
.
filled
(
child:
const
Icon
(
CupertinoIcons
.
search
,
size:
28.0
,
color:
CupertinoColors
.
white
,
),
padding:
EdgeInsets
.
zero
,
onPressed:
onPressed
,
);
},
shortcuts:
widget
.
shortcuts
,
actions:
widget
.
actions
,
),
);
},
child:
HeroControllerScope
(
controller:
_heroController
,
child:
Builder
(
builder:
_buildWidgetApp
,
),
),
),
),
...
...
packages/flutter/lib/src/material/app.dart
View file @
f9fd71bc
...
...
@@ -7,6 +7,7 @@
import
'dart:ui'
as
ui
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/widgets.dart'
;
...
...
@@ -205,6 +206,58 @@ class MaterialApp extends StatefulWidget {
assert
(
checkerboardOffscreenLayers
!=
null
),
assert
(
showSemanticsDebugger
!=
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
);
/// {@macro flutter.widgets.widgetsApp.navigatorKey}
...
...
@@ -238,6 +291,18 @@ class MaterialApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.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}
///
/// Material specific features such as [showDialog] and [showMenu], and widgets
...
...
@@ -611,6 +676,8 @@ class _MaterialScrollBehavior extends ScrollBehavior {
class
_MaterialAppState
extends
State
<
MaterialApp
>
{
HeroController
_heroController
;
bool
get
_usesRouter
=>
widget
.
routerDelegate
!=
null
;
@override
void
initState
()
{
super
.
initState
();
...
...
@@ -629,75 +696,77 @@ class _MaterialAppState extends State<MaterialApp> {
yield
DefaultCupertinoLocalizations
.
delegate
;
}
@override
Widget
build
(
BuildContext
context
)
{
Widget
result
=
HeroControllerScope
(
controller:
_heroController
,
child:
WidgetsApp
(
Widget
_inspectorSelectButtonBuilder
(
BuildContext
context
,
VoidCallback
onPressed
)
{
return
FloatingActionButton
(
child:
const
Icon
(
Icons
.
search
),
onPressed:
onPressed
,
mini:
true
,
);
}
Widget
_materialBuilder
(
BuildContext
context
,
Widget
child
)
{
// Resolve which theme to use based on brightness and high contrast.
final
ThemeMode
mode
=
widget
.
themeMode
??
ThemeMode
.
system
;
final
Brightness
platformBrightness
=
MediaQuery
.
platformBrightnessOf
(
context
);
final
bool
useDarkTheme
=
mode
==
ThemeMode
.
dark
||
(
mode
==
ThemeMode
.
system
&&
platformBrightness
==
ui
.
Brightness
.
dark
);
final
bool
highContrast
=
MediaQuery
.
highContrastOf
(
context
);
ThemeData
theme
;
if
(
useDarkTheme
&&
highContrast
&&
widget
.
highContrastDarkTheme
!=
null
)
{
theme
=
widget
.
highContrastDarkTheme
;
}
else
if
(
useDarkTheme
&&
widget
.
darkTheme
!=
null
)
{
theme
=
widget
.
darkTheme
;
}
else
if
(
highContrast
&&
widget
.
highContrastTheme
!=
null
)
{
theme
=
widget
.
highContrastTheme
;
}
theme
??=
widget
.
theme
??
ThemeData
.
light
();
return
AnimatedTheme
(
data:
theme
,
isMaterialAppTheme:
true
,
child:
widget
.
builder
!=
null
?
Builder
(
builder:
(
BuildContext
context
)
{
// Why are we surrounding a builder with a builder?
//
// The widget.builder may contain code that invokes
// Theme.of(), which should return the theme we selected
// above in AnimatedTheme. However, if we invoke
// widget.builder() directly as the child of AnimatedTheme
// then there is no Context separating them, and the
// widget.builder() will not find the theme. Therefore, we
// surround widget.builder with yet another builder so that
// a context separates them and Theme.of() correctly
// resolves to the theme we passed to AnimatedTheme.
return
widget
.
builder
(
context
,
child
);
},
)
:
child
,
);
}
Widget
_buildWidgetApp
(
BuildContext
context
)
{
// The color property is always pulled from the light theme, even if dark
// mode is activated. This was done to simplify the technical details
// 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
// Android's switcher UI.
//
// blue is the primary color of the default theme.
final
Color
materialColor
=
widget
.
color
??
widget
.
theme
?.
primaryColor
??
Colors
.
blue
;
if
(
_usesRouter
)
{
return
WidgetsApp
.
router
(
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:
(
BuildContext
context
,
Widget
child
)
{
// Resolve which theme to use based on brightness and high contrast.
final
ThemeMode
mode
=
widget
.
themeMode
??
ThemeMode
.
system
;
final
Brightness
platformBrightness
=
MediaQuery
.
platformBrightnessOf
(
context
);
final
bool
useDarkTheme
=
mode
==
ThemeMode
.
dark
||
(
mode
==
ThemeMode
.
system
&&
platformBrightness
==
ui
.
Brightness
.
dark
);
final
bool
highContrast
=
MediaQuery
.
highContrastOf
(
context
);
ThemeData
theme
;
if
(
useDarkTheme
&&
highContrast
&&
widget
.
highContrastDarkTheme
!=
null
)
{
theme
=
widget
.
highContrastDarkTheme
;
}
else
if
(
useDarkTheme
&&
widget
.
darkTheme
!=
null
)
{
theme
=
widget
.
darkTheme
;
}
else
if
(
highContrast
&&
widget
.
highContrastTheme
!=
null
)
{
theme
=
widget
.
highContrastTheme
;
}
theme
??=
widget
.
theme
??
ThemeData
.
light
();
return
AnimatedTheme
(
data:
theme
,
isMaterialAppTheme:
true
,
child:
widget
.
builder
!=
null
?
Builder
(
builder:
(
BuildContext
context
)
{
// Why are we surrounding a builder with a builder?
//
// The widget.builder may contain code that invokes
// Theme.of(), which should return the theme we selected
// above in AnimatedTheme. However, if we invoke
// widget.builder() directly as the child of AnimatedTheme
// then there is no Context separating them, and the
// widget.builder() will not find the theme. Therefore, we
// surround widget.builder with yet another builder so that
// a context separates them and Theme.of() correctly
// resolves to the theme we passed to AnimatedTheme.
return
widget
.
builder
(
context
,
child
);
},
)
:
child
,
);
},
routeInformationProvider:
widget
.
routeInformationProvider
,
routeInformationParser:
widget
.
routeInformationParser
,
routerDelegate:
widget
.
routerDelegate
,
backButtonDispatcher:
widget
.
backButtonDispatcher
,
builder:
_materialBuilder
,
title:
widget
.
title
,
onGenerateTitle:
widget
.
onGenerateTitle
,
textStyle:
_errorTextStyle
,
// The color property is always pulled from the light theme, even if dark
// mode is activated. This was done to simplify the technical details
// 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
// Android's switcher UI.
//
// blue is the primary color of the default theme
color:
widget
.
color
??
widget
.
theme
?.
primaryColor
??
Colors
.
blue
,
color:
materialColor
,
locale:
widget
.
locale
,
localizationsDelegates:
_localizationsDelegates
,
localeResolutionCallback:
widget
.
localeResolutionCallback
,
...
...
@@ -708,17 +777,49 @@ class _MaterialAppState extends State<MaterialApp> {
checkerboardOffscreenLayers:
widget
.
checkerboardOffscreenLayers
,
showSemanticsDebugger:
widget
.
showSemanticsDebugger
,
debugShowCheckedModeBanner:
widget
.
debugShowCheckedModeBanner
,
inspectorSelectButtonBuilder:
(
BuildContext
context
,
VoidCallback
onPressed
)
{
return
FloatingActionButton
(
child:
const
Icon
(
Icons
.
search
),
onPressed:
onPressed
,
mini:
true
,
);
},
inspectorSelectButtonBuilder:
_inspectorSelectButtonBuilder
,
shortcuts:
widget
.
shortcuts
,
actions:
widget
.
actions
,
),
);
}
return
WidgetsApp
(
key:
GlobalObjectKey
(
this
),
navigatorKey:
widget
.
navigatorKey
,
navigatorObservers:
widget
.
navigatorObservers
,
pageRouteBuilder:
<
T
>(
RouteSettings
settings
,
WidgetBuilder
builder
)
{
return
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
(()
{
if
(
widget
.
debugShowMaterialGrid
)
{
...
...
@@ -735,7 +836,10 @@ class _MaterialAppState extends State<MaterialApp> {
return
ScrollConfiguration
(
behavior:
_MaterialScrollBehavior
(),
child:
result
,
child:
HeroControllerScope
(
controller:
_heroController
,
child:
result
,
)
);
}
}
packages/flutter/lib/src/services/system_channels.dart
View file @
f9fd71bc
...
...
@@ -26,16 +26,18 @@ class SystemChannels {
/// * `pushRoute`, which is called with a single string argument when the
/// 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
/// framework notifies the engine about the route changes.
///
/// * `routePushed`, which is called when a route is pushed. (e.g. A modal
/// replaces the entire screen.)
///
/// * `routePopped`, which is called when a route is popped. (e.g. A dialog,
/// such as time picker is closed.)
/// * `routeUpdated`, which is called when current route has changed.
///
/// * `routeReplaced`, which is called when a route is replaced.
/// * `routeInformationUpdated`, which is called by the [Router] when the
/// application navigate to a new location.
///
/// See also:
///
...
...
@@ -46,7 +48,7 @@ class SystemChannels {
/// [Navigator.push], [Navigator.pushReplacement], [Navigator.pop] and
/// [Navigator.replace], utilize this channel's methods to send route
/// change information from framework to engine.
static
const
MethodChannel
navigation
=
MethodChannel
(
static
const
MethodChannel
navigation
=
Optional
MethodChannel
(
'flutter/navigation'
,
JSONMethodCodec
(),
);
...
...
packages/flutter/lib/src/services/system_navigator.dart
View file @
f9fd71bc
...
...
@@ -35,4 +35,37 @@ class SystemNavigator {
static
Future
<
void
>
pop
({
bool
?
animated
})
async
{
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
,
},
);
}
}
packages/flutter/lib/src/widgets/app.dart
View file @
f9fd71bc
...
...
@@ -22,6 +22,7 @@ import 'media_query.dart';
import
'navigator.dart'
;
import
'pages.dart'
;
import
'performance_overlay.dart'
;
import
'router.dart'
;
import
'scrollable.dart'
;
import
'semantics_debugger.dart'
;
import
'shortcuts.dart'
;
...
...
@@ -260,6 +261,62 @@ class WidgetsApp extends StatefulWidget {
assert
(
showSemanticsDebugger
!=
null
),
assert
(
debugShowCheckedModeBanner
!=
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
);
/// {@template flutter.widgets.widgetsApp.navigatorKey}
...
...
@@ -321,6 +378,71 @@ class WidgetsApp extends StatefulWidget {
/// or a [CupertinoPageRoute] should be used for building page transitions.
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}
/// The widget for the default route of the app ([Navigator.defaultRouteName],
/// which is `/`).
...
...
@@ -960,10 +1082,21 @@ class WidgetsApp extends StatefulWidget {
class
_WidgetsAppState
extends
State
<
WidgetsApp
>
with
WidgetsBindingObserver
{
// 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
void
initState
()
{
super
.
initState
();
_updateNavigator
();
if
(
_usesRouter
)
{
_updateRouter
();
}
else
{
_updateNavigator
();
}
_locale
=
_resolveLocales
(
WidgetsBinding
.
instance
.
window
.
locales
,
widget
.
supportedLocales
);
WidgetsBinding
.
instance
.
addObserver
(
this
);
}
...
...
@@ -971,16 +1104,37 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override
void
didUpdateWidget
(
WidgetsApp
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
widget
.
navigatorKey
!=
oldWidget
.
navigatorKey
)
if
(
oldWidget
.
routeInformationProvider
!=
widget
.
routeInformationProvider
)
{
_updateRouter
();
}
if
(
widget
.
navigatorKey
!=
oldWidget
.
navigatorKey
)
{
_updateNavigator
();
}
}
@override
void
dispose
()
{
WidgetsBinding
.
instance
.
removeObserver
(
this
);
_defaultRouteInformationProvider
?.
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
GlobalKey
<
NavigatorState
>
_navigator
;
...
...
@@ -1050,6 +1204,11 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override
Future
<
bool
>
didPopRoute
()
async
{
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
;
if
(
navigator
==
null
)
return
false
;
...
...
@@ -1059,6 +1218,11 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override
Future
<
bool
>
didPushRoute
(
String
route
)
async
{
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
;
if
(
navigator
==
null
)
return
false
;
...
...
@@ -1291,16 +1455,20 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override
Widget
build
(
BuildContext
context
)
{
Widget
navigator
;
if
(
_navigator
!=
null
)
{
navigator
=
Navigator
(
Widget
routing
;
if
(
_usesRouter
)
{
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
,
// If window.defaultRouteName isn't '/', we should assume it was set
// 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
,
initialRoute:
_initialRouteName
,
onGenerateRoute:
_onGenerateRoute
,
onGenerateInitialRoutes:
widget
.
onGenerateInitialRoutes
==
null
?
Navigator
.
defaultGenerateInitialRoutes
...
...
@@ -1317,12 +1485,12 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
if
(
widget
.
builder
!=
null
)
{
result
=
Builder
(
builder:
(
BuildContext
context
)
{
return
widget
.
builder
(
context
,
navigator
);
return
widget
.
builder
(
context
,
routing
);
},
);
}
else
{
assert
(
navigator
!=
null
);
result
=
navigator
;
assert
(
routing
!=
null
);
result
=
routing
;
}
if
(
widget
.
textStyle
!=
null
)
{
...
...
packages/flutter/lib/src/widgets/binding.dart
View file @
f9fd71bc
...
...
@@ -18,6 +18,7 @@ import 'app.dart';
import
'debug.dart'
;
import
'focus_manager.dart'
;
import
'framework.dart'
;
import
'router.dart'
;
import
'widget_inspector.dart'
;
export
'dart:ui'
show
AppLifecycleState
,
Locale
;
...
...
@@ -95,7 +96,7 @@ abstract class WidgetsBindingObserver {
/// [SystemChannels.navigation].
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 app
lication
to push a new route onto the
/// navigator.
///
/// Observers are expected to return true if they were able to
...
...
@@ -106,6 +107,22 @@ abstract class WidgetsBindingObserver {
/// [SystemChannels.navigation].
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,
/// 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
)
{
switch
(
methodCall
.
method
)
{
case
'popRoute'
:
return
handlePopRoute
();
case
'pushRoute'
:
return
handlePushRoute
(
methodCall
.
arguments
as
String
);
case
'pushRouteInformation'
:
return
_handlePushRouteInformation
(
methodCall
.
arguments
as
Map
<
dynamic
,
dynamic
>);
}
return
Future
<
dynamic
>.
value
();
}
...
...
packages/flutter/lib/src/widgets/navigator.dart
View file @
f9fd71bc
...
...
@@ -21,7 +21,6 @@ import 'focus_scope.dart';
import
'framework.dart'
;
import
'heroes.dart'
;
import
'overlay.dart'
;
import
'route_notification_messages.dart'
;
import
'routes.dart'
;
import
'ticker_provider.dart'
;
...
...
@@ -3308,8 +3307,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_RouteEntry
.
isPresentPredicate
,
orElse:
()
=>
null
);
final
String
routeName
=
lastEntry
?.
route
?.
settings
?.
name
;
if
(
routeName
!=
_lastAnnouncedRouteName
)
{
RouteNotificationMessages
.
maybeNotifyRouteChange
(
routeName
,
_lastAnnouncedRouteName
);
SystemNavigator
.
routeUpdated
(
routeName:
routeName
,
previousRouteName:
_lastAnnouncedRouteName
);
_lastAnnouncedRouteName
=
routeName
;
}
}
...
...
packages/flutter/lib/src/widgets/route_notification_messages.dart
deleted
100644 → 0
View file @
12b8d9db
// 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
,
},
);
}
}
packages/flutter/lib/src/widgets/router.dart
0 → 100644
View file @
f9fd71bc
// 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
'dart:async'
;
import
'dart:collection'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'package:flutter/services.dart'
;
import
'basic.dart'
;
import
'binding.dart'
;
import
'framework.dart'
;
import
'navigator.dart'
;
/// A piece of routing information.
///
/// The route information consists of a location string of the application and
/// a state object that configures the application in that location.
///
/// This information flows two ways, from the [RouteInformationProvider] to the
/// [Router] or from the [Router] to [RouteInformationProvider].
///
/// In the former case, the [RouteInformationProvider] notifies the [Router]
/// widget when a new [RouteInformation] is available. The [Router] widget takes
/// these information and navigates accordingly.
///
/// The latter case should only happen in a web application where the [Router]
/// reports route change back to web engine.
class
RouteInformation
{
/// Creates a route information.
const
RouteInformation
({
this
.
location
,
this
.
state
});
/// The location of the application.
///
/// The string is usually in the format of multiple string identifiers with
/// slashes in between. ex: `/`, `/path`, `/path/to/the/app`.
///
/// It is equivalent to the URL in a web application.
final
String
location
;
/// The state of the application in the [location].
///
/// The app can have different states even in the same location. For example
/// the text inside a [TextField] or the scroll position in a [ScrollView],
/// these widget states can be stored in the [state].
///
/// It's only used in the web application currently. In a web application,
/// this property is stored into browser history entry when the [Router]
/// report this route information back to the web engine through the
/// [PlatformRouteInformationProvider], so we can get the url along with state
/// back when the user click the forward or backward buttons.
///
/// The state must be serializable.
final
Object
state
;
}
/// The dispatcher for opening and closing pages of an application.
///
/// This widget listens for routing information from the operating system (e.g.
/// an initial route provided on app startup, a new route obtained when an
/// intent is received, or a notification that the user hit the system back
/// button), parses route information into data of type `T`, and then converts
/// that data into [Page] objects that it passes to a [Navigator].
///
/// Additionally, every single part of that previous sentence can be overridden
/// and configured as desired.
///
/// The [routeInformationProvider] can be overridden to change how the name of
/// the route is obtained. the [RouteInformationProvider.value] when the
/// [Router] is first created is used as the initial route, and subsequent
/// notifications from the [RouteInformationProvider] to its listeners are
/// treated as notifications that the route information has changed.
///
/// The [backButtonDispatcher] can be overridden to change how back button
/// notifications are received. This must be a [BackButtonDispatcher], which is
/// an object where callbacks can be registered, and which can be chained
/// so that back button presses are delegated to subsidiary routers. The
/// callbacks are invoked to indicate that the user is trying to close the
/// current route (by pressing the system back button); the [Router] ensures
/// that when this callback is invoked, the message is passed to the
/// [routerDelegate] and its result is provided back to the
/// [backButtonDispatcher]. Some platforms don't have back buttons and on those
/// platforms it is completely normal that this notification is never sent. The
/// common [backButtonDispatcher] for root router is an instance of
/// [RootBackButtonDispatcher], which uses a [WidgetsBindingObserver] to listen
/// to the `popRoute` notifications from [SystemChannels.navigation]. A
/// common alternative is [ChildBackButtonDispatcher], which must be provided
/// the [BackButtonDispatcher] of its ancestor [Router] (available via
/// [Router.of]).
///
/// The [routeInformationParser] can be overridden to change how names obtained
/// from the [routeInformationProvider] are interpreted. It must implement the
/// [RouteInformationParser] interface, specialized with the same type as the
/// [Router] itself. This type, `T`, represents the data type that the
/// [routeInformationParser] will generate.
///
/// The [routerDelegate] can be overridden to change how the output of the
/// [routeInformationParser] is interpreted. It must implement the
/// [RouterDelegate] interface, also specialized with `T`; it takes as input
/// the data (of type `T`) from the [routeInformationParser], and is responsible
/// for providing a navigating widget to insert into the widget tree. The
/// [RouterDelegate] interface is also [Listenable]; notifications are taken
/// to mean that the [Router] needs to rebuild.
///
/// ## Concerns regarding asynchrony
///
/// Some of the APIs (notably those involving [RouteInformationParser] and
/// [RouterDelegate]) are asynchronous.
///
/// When developing objects implementing these APIs, if the work can be done
/// entirely synchronously, then consider using [SynchronousFuture] for the
/// future returned from the relevant methods. This will allow the [Router] to
/// proceed in a completely synchronous way, which removes a number of
/// complications.
///
/// Using asynchronous computation is entirely reasonable, however, and the API
/// is designed to support it. For example, maybe a set of images need to be
/// loaded before a route can be shown; waiting for those images to be loaded
/// before [RouterDelegate.setNewRoutePath] returns is a reasonable approach to
/// handle this case.
///
/// If an asynchronous operation is ongoing when a new one is to be started, the
/// precise behavior will depend on the exact circumstances, as follows:
///
/// If the active operation is a [routeInformationParser] parsing a new route information:
/// that operation's result, if it ever completes, will be discarded.
///
/// If the active operation is a [routerDelegate] handling a pop request:
/// the previous pop is immediately completed with "false", claiming that the
/// previous pop was not handled (this may cause the application to close).
///
/// If the active operation is a [routerDelegate] handling an initial route
/// or a pushed route, the result depends on the new operation. If the new
/// operation is a pop request, then the original operation's result, if it ever
/// completes, will be discarded. If the new operation is a push request,
/// however, the [routeInformationParser] will be requested to start the parsing, and
/// only if that finishes before the original [routerDelegate] request
/// completes will that original request's result be discarded.
///
/// If the identity of the [Router] widget's delegates change while an
/// asynchronous operation is in progress, to keep matters simple, all active
/// asynchronous operations will have their results discarded. It is generally
/// considered unusual for these delegates to change during the lifetime of the
/// [Router].
///
/// If the [Router] itself is disposed while an an asynchronous operation is in
/// progress, all active asynchronous operations will have their results
/// discarded also.
///
/// No explicit signals are provided to the [routeInformationParser] or
/// [routerDelegate] to indicate when any of the above happens, so it is
/// strongly recommended that [RouteInformationParser] and [RouterDelegate]
/// implementations not perform extensive computation.
///
/// ## Application architectural design
///
/// An application can have zero, one, or many [Router] widgets, depending on
/// its needs.
///
/// An application might have no [Router] widgets if it has only one "screen",
/// or if the facilities provided by [Navigator] are sufficient.
///
/// A particularly elaborate application might have multiple [Router] widgets,
/// in a tree configuration, with the first handling the entire route parsing
/// and making the result available for routers in the subtree. The routers in
/// the subtree do not participate in route information parsing but merely take the
/// result from the first router to build their sub routes.
///
/// Most applications only need a single [Router].
///
/// ## URL updates for web applications
///
/// In the web platform, it is important to keeps the URL up to date with the
/// app state. This ensures the browser constructs its history entry
/// correctly so that its forward and backward buttons continue to work.
///
/// If the [routeInformationProvider] is a [PlatformRouteInformationProvider]
/// and a app state change leads to [Router] rebuilds, the [Router] will detect
/// such a event and retrieve the new route information from the
/// [RouterDelegate.currentConfiguration] and the
/// [RouteInformationParser.restoreRouteInformation]. If the location in the
/// new route information is different from the current location, the router
/// sends the new route information to the engine through the
/// [PlatformRouteInformationProvider.routerReportsNewRouteInformation].
///
/// By Providing implementations of these two methods in the subclasses and using
/// the [PlatformRouteInformationProvider], you can enable the [Router] widget to
/// update the URL in the browser automatically.
///
/// You can force the [Router] to report the new route information back to the
/// engine even if the [RouteInformation.location] has not changed. By calling
/// the [Router.navigate], the [Router] will be forced to report the route
/// information back to the engine after running the callback. This is useful
/// when you want to support the browser backward and forward buttons without
/// changing the URL. For example, the scroll position of a scroll view may be
/// saved in the [RouteInformation.state]. If you use the [Router.navigate] to
/// update the scroll position, the browser will create a new history entry with
/// the [RouteInformation.state] that stores the new scroll position. when the
/// users click the backward button, the browser will go back to previous scroll
/// position without changing the url bar.
///
/// You can also force the [Router] to ignore a one time route information
/// update by providing a one time app state update in a callback and pass it
/// into the [Router.neglect]. The [Router] will not report any route
/// information even if it detects location change as a result of running the
/// callback. This is particularly useful when you don't want the browser to
/// create a browser history entry for this app state update.
///
/// You can also choose to opt out of URL updates entirely. Simply ignore the
/// [RouterDelegate.currentConfiguration] and the
/// [RouteInformationParser.restoreRouteInformation] without providing the
/// implementations will prevent the [Router] from reporting the URL back to the
/// web engine. This is not recommended in general, but You may decide to opt
/// out in these cases:
///
/// * If you are not writing a web application.
///
/// * If you have multiple router widgets in your app, then only one router
/// widget should update the URL (Usually the top-most one created by the
/// [WidgetsApp.router]/[MaterialApp.router]/[CupertinoApp.router]).
///
/// * If your app does not care about the in-app navigation using the browser's
/// forward and backward buttons.
///
/// Otherwise, we strongly recommend implementing the
/// [RouterDelegate.currentConfiguration] and the
/// [RouteInformationParser.restoreRouteInformation] to provide optimal
/// user experience in the web application.
class
Router
<
T
>
extends
StatefulWidget
{
/// Creates a router.
///
/// The [routeInformationProvider] and [routeInformationParser] can be null if this
/// router does not depend on route information. A common example is a sub router
/// that builds its content completely relies on the app state.
///
/// If the [routeInformationProvider] is not null, the [routeInformationParser] must
/// also not be null.
///
/// The [routerDelegate] must not be null.
const
Router
({
Key
key
,
this
.
routeInformationProvider
,
this
.
routeInformationParser
,
@required
this
.
routerDelegate
,
this
.
backButtonDispatcher
,
})
:
assert
(
routeInformationProvider
==
null
||
routeInformationParser
!=
null
),
assert
(
routerDelegate
!=
null
),
super
(
key:
key
);
/// The route information provider for the router.
///
/// The value at the time of first build will be used as the initial route.
/// The [Router] listens to this provider and rebuilds with new names when
/// it notifies.
///
/// This can be null if this router does not rely on the route information
/// to build its content. In such case, the [routeInformationParser] can also be
/// null.
final
RouteInformationProvider
routeInformationProvider
;
/// The route information parser for the router.
///
/// When the [Router] gets a new route information from the [routeInformationProvider],
/// the [Router] uses this delegate to parse the route information and produce a
/// configuration. The configuration will be used by [routerDelegate] and
/// eventually rebuilds the [Router] widget.
///
/// Since this delegate is the primary consumer of the [routeInformationProvider],
/// it must not be null if [routeInformationProvider] is not null.
final
RouteInformationParser
<
T
>
routeInformationParser
;
/// The router delegate for the router.
///
/// This delegate consumes the configuration from [routeInformationParser] and
/// builds a navigating widget for the [Router].
///
/// It is also the primary respondent for the [backButtonDispatcher]. The
/// [Router] relies on the [RouterDelegate.popRoute] to handles the back
/// button intends.
///
/// If the [RouterDelegate.currentConfiguration] returns a non-null object,
/// this [Router] will opt for URL updates.
final
RouterDelegate
<
T
>
routerDelegate
;
/// The back button dispatcher for the router.
///
/// The two common alternatives are the [RootBackButtonDispatcher] for root
/// router, or the [ChildBackButtonDispatcher] for other routers.
final
BackButtonDispatcher
backButtonDispatcher
;
/// Retrieves the immediate [Router] ancestor from the given context.
///
/// Use this method when you need to access the delegates in the [Router].
/// For example, you need to access the [backButtonDispatcher] of the parent
/// router to create a [ChildBackButtonDispatcher] for a nested router.
/// Another use case may be updating the value in [routeInformationProvider]
/// to navigate to a new route.
static
Router
<
dynamic
>
of
(
BuildContext
context
)
{
final
_RouterScope
scope
=
context
.
dependOnInheritedWidgetOfExactType
<
_RouterScope
>();
assert
(
scope
!=
null
);
return
scope
.
routerState
.
widget
;
}
/// Forces the [Router] to run the [callback] and reports the route
/// information back to the engine.
///
/// The web application relies on the [Router] to report new route information
/// in order to create browser history entry. The [Router] will only report
/// them if it detects the [RouteInformation.location] changes. Use this
/// method if you want the [Router] to report the route information even if
/// the location does not change. This can be useful when you want to
/// support the browser backward and forward button without changing the URL.
///
/// For example, you can store certain state such as the scroll position into
/// the [RouteInformation.state]. If you use this method to update the
/// scroll position multiple times with the same URL, the browser will create
/// a stack of new history entries with the same URL but different
/// [RouteInformation.state]s that store the new scroll positions. If the user
/// click the backward button in the browser, the browser will restore the
/// scroll positions saved in history entries without changing the URL.
///
/// See also:
///
/// * [Router]: see the "URL updates for web applications" section for more
/// information about route information reporting.
/// * [neglect]: which forces the [Router] to not report the route
/// information even if location does change.
static
void
navigate
(
BuildContext
context
,
VoidCallback
callback
)
{
final
_RouterScope
scope
=
context
.
getElementForInheritedWidgetOfExactType
<
_RouterScope
>()
.
widget
as
_RouterScope
;
scope
.
routerState
.
_setStateWithExplicitReportStatus
(
_IntentionToReportRouteInformation
.
must
,
callback
);
}
/// Forces the [Router] to to run the [callback] without reporting the route
/// information back to the engine.
///
/// Use this method if you don't want the [Router] to report the new route
/// information even if it detects changes as a result of running the
/// [callback].
///
/// The web application relies on the [Router] to report new route information
/// in order to create browser history entry. The [Router] will report them
/// automatically if it detects the [RouteInformation.location] changes. You
/// can use this method if you want to navigate to a new route without
/// creating the browser history entry.
///
/// See also:
///
/// * [Router]: see the "URL updates for web applications" section for more
/// information about route information reporting.
/// * [navigate]: which forces the [Router] to report the route information
/// even if location does not change.
static
void
neglect
(
BuildContext
context
,
VoidCallback
callback
)
{
final
_RouterScope
scope
=
context
.
getElementForInheritedWidgetOfExactType
<
_RouterScope
>()
.
widget
as
_RouterScope
;
scope
.
routerState
.
_setStateWithExplicitReportStatus
(
_IntentionToReportRouteInformation
.
ignore
,
callback
);
}
@override
State
<
Router
<
T
>>
createState
()
=>
_RouterState
<
T
>();
}
typedef
_AsyncPassthrough
<
Q
>
=
Future
<
Q
>
Function
(
Q
);
// Whether to report the route information in this build cycle.
enum
_IntentionToReportRouteInformation
{
// We haven't receive any signal on whether to report.
none
,
// Report if route information changes.
maybe
,
// Report regardless of route information changes.
must
,
// Don't report regardless of route information changes.
ignore
,
}
class
_RouterState
<
T
>
extends
State
<
Router
<
T
>>
{
Object
_currentRouteInformationParserTransaction
;
Object
_currentRouterDelegateTransaction
;
_IntentionToReportRouteInformation
_currentIntentionToReport
;
@override
void
initState
()
{
super
.
initState
();
widget
.
routeInformationProvider
?.
addListener
(
_handleRouteInformationProviderNotification
);
widget
.
backButtonDispatcher
?.
addCallback
(
_handleBackButtonDispatcherNotification
);
widget
.
routerDelegate
.
addListener
(
_handleRouterDelegateNotification
);
_currentIntentionToReport
=
_IntentionToReportRouteInformation
.
none
;
if
(
widget
.
routeInformationProvider
!=
null
)
{
_processInitialRoute
();
}
_lastSeenLocation
=
widget
.
routeInformationProvider
?.
value
?.
location
;
}
bool
_routeInformationReportingTaskScheduled
=
false
;
String
_lastSeenLocation
;
void
_scheduleRouteInformationReportingTask
()
{
if
(
_routeInformationReportingTaskScheduled
)
return
;
assert
(
_currentIntentionToReport
!=
_IntentionToReportRouteInformation
.
none
);
_routeInformationReportingTaskScheduled
=
true
;
SchedulerBinding
.
instance
.
addPostFrameCallback
(
_reportRouteInformation
);
}
void
_reportRouteInformation
(
Duration
timestamp
)
{
assert
(
_routeInformationReportingTaskScheduled
);
_routeInformationReportingTaskScheduled
=
false
;
switch
(
_currentIntentionToReport
)
{
case
_IntentionToReportRouteInformation
.
none
:
assert
(
false
);
return
;
case
_IntentionToReportRouteInformation
.
ignore
:
// In the ignore case, we still want to update the _lastSeenLocation.
final
RouteInformation
routeInformation
=
_retrieveNewRouteInformation
();
if
(
routeInformation
!=
null
)
{
_lastSeenLocation
=
routeInformation
.
location
;
}
_currentIntentionToReport
=
_IntentionToReportRouteInformation
.
none
;
return
;
case
_IntentionToReportRouteInformation
.
maybe
:
final
RouteInformation
routeInformation
=
_retrieveNewRouteInformation
();
if
(
routeInformation
!=
null
)
{
if
(
_lastSeenLocation
!=
routeInformation
.
location
)
{
widget
.
routeInformationProvider
.
routerReportsNewRouteInformation
(
routeInformation
);
_lastSeenLocation
=
routeInformation
.
location
;
}
}
_currentIntentionToReport
=
_IntentionToReportRouteInformation
.
none
;
return
;
case
_IntentionToReportRouteInformation
.
must
:
final
RouteInformation
routeInformation
=
_retrieveNewRouteInformation
();
if
(
routeInformation
!=
null
)
{
widget
.
routeInformationProvider
.
routerReportsNewRouteInformation
(
routeInformation
);
_lastSeenLocation
=
routeInformation
.
location
;
}
_currentIntentionToReport
=
_IntentionToReportRouteInformation
.
none
;
return
;
}
}
RouteInformation
_retrieveNewRouteInformation
()
{
final
T
configuration
=
widget
.
routerDelegate
.
currentConfiguration
;
if
(
configuration
==
null
)
return
null
;
final
RouteInformation
routeInformation
=
widget
.
routeInformationParser
.
restoreRouteInformation
(
configuration
);
assert
((){
if
(
routeInformation
==
null
)
{
FlutterError
.
reportError
(
const
FlutterErrorDetails
(
exception:
'Router.routeInformationParser returns a null RouteInformation. '
'If you opt for route information reporting, the '
'routeInformationParser must not report null for a given '
'configuration.'
),
);
}
return
true
;
}());
return
routeInformation
;
}
void
_setStateWithExplicitReportStatus
(
_IntentionToReportRouteInformation
status
,
VoidCallback
fn
,
)
{
assert
(
status
!=
null
);
assert
(
status
.
index
>=
_IntentionToReportRouteInformation
.
must
.
index
);
assert
(()
{
if
(
_currentIntentionToReport
.
index
>=
_IntentionToReportRouteInformation
.
must
.
index
&&
_currentIntentionToReport
!=
status
)
{
FlutterError
.
reportError
(
const
FlutterErrorDetails
(
exception:
'Both Router.navigate and Router.neglect have been called in this '
'build cycle, and the Router cannot decide whether to report the '
'route information. Please make sure only one of them is called '
'within the same build cycle.'
),
);
}
return
true
;
}());
_currentIntentionToReport
=
status
;
_scheduleRouteInformationReportingTask
();
fn
();
}
void
_maybeNeedToReportRouteInformation
()
{
_currentIntentionToReport
=
_currentIntentionToReport
!=
_IntentionToReportRouteInformation
.
none
?
_currentIntentionToReport
:
_IntentionToReportRouteInformation
.
maybe
;
_scheduleRouteInformationReportingTask
();
}
@override
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
_maybeNeedToReportRouteInformation
();
}
@override
void
didUpdateWidget
(
Router
<
T
>
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
widget
.
routeInformationProvider
!=
oldWidget
.
routeInformationProvider
||
widget
.
backButtonDispatcher
!=
oldWidget
.
backButtonDispatcher
||
widget
.
routeInformationParser
!=
oldWidget
.
routeInformationParser
||
widget
.
routerDelegate
!=
oldWidget
.
routerDelegate
)
{
_currentRouteInformationParserTransaction
=
Object
();
_currentRouterDelegateTransaction
=
Object
();
}
if
(
widget
.
routeInformationProvider
!=
oldWidget
.
routeInformationProvider
)
{
oldWidget
.
routeInformationProvider
?.
removeListener
(
_handleRouteInformationProviderNotification
);
widget
.
routeInformationProvider
?.
addListener
(
_handleRouteInformationProviderNotification
);
if
(
oldWidget
.
routeInformationProvider
?.
value
!=
widget
.
routeInformationProvider
?.
value
)
{
_handleRouteInformationProviderNotification
();
}
}
if
(
widget
.
backButtonDispatcher
!=
oldWidget
.
backButtonDispatcher
)
{
oldWidget
.
backButtonDispatcher
?.
removeCallback
(
_handleBackButtonDispatcherNotification
);
widget
.
backButtonDispatcher
?.
addCallback
(
_handleBackButtonDispatcherNotification
);
}
if
(
widget
.
routerDelegate
!=
oldWidget
.
routerDelegate
)
{
oldWidget
.
routerDelegate
.
removeListener
(
_handleRouterDelegateNotification
);
widget
.
routerDelegate
.
addListener
(
_handleRouterDelegateNotification
);
_maybeNeedToReportRouteInformation
();
}
}
@override
void
dispose
()
{
widget
.
routeInformationProvider
?.
removeListener
(
_handleRouteInformationProviderNotification
);
widget
.
backButtonDispatcher
?.
removeCallback
(
_handleBackButtonDispatcherNotification
);
widget
.
routerDelegate
.
removeListener
(
_handleRouterDelegateNotification
);
_currentRouteInformationParserTransaction
=
null
;
_currentRouterDelegateTransaction
=
null
;
super
.
dispose
();
}
void
_processInitialRoute
()
{
_currentRouteInformationParserTransaction
=
Object
();
_currentRouterDelegateTransaction
=
Object
();
widget
.
routeInformationParser
.
parseRouteInformation
(
widget
.
routeInformationProvider
.
value
)
.
then
<
T
>(
_verifyRouteInformationParserStillCurrent
(
_currentRouteInformationParserTransaction
,
widget
))
.
then
<
void
>(
widget
.
routerDelegate
.
setInitialRoutePath
)
.
then
<
void
>(
_verifyRouterDelegatePushStillCurrent
(
_currentRouterDelegateTransaction
,
widget
))
.
then
<
void
>(
_rebuild
);
}
void
_handleRouteInformationProviderNotification
()
{
_currentRouteInformationParserTransaction
=
Object
();
_currentRouterDelegateTransaction
=
Object
();
widget
.
routeInformationParser
.
parseRouteInformation
(
widget
.
routeInformationProvider
.
value
)
.
then
<
T
>(
_verifyRouteInformationParserStillCurrent
(
_currentRouteInformationParserTransaction
,
widget
))
.
then
<
void
>(
widget
.
routerDelegate
.
setNewRoutePath
)
.
then
<
void
>(
_verifyRouterDelegatePushStillCurrent
(
_currentRouterDelegateTransaction
,
widget
))
.
then
<
void
>(
_rebuild
);
}
Future
<
bool
>
_handleBackButtonDispatcherNotification
()
{
_currentRouteInformationParserTransaction
=
Object
();
_currentRouterDelegateTransaction
=
Object
();
return
widget
.
routerDelegate
.
popRoute
()
.
then
<
bool
>(
_verifyRouterDelegatePopStillCurrent
(
_currentRouterDelegateTransaction
,
widget
))
.
then
<
bool
>((
bool
data
)
{
_rebuild
();
_maybeNeedToReportRouteInformation
();
return
SynchronousFuture
<
bool
>(
data
);
});
}
static
final
Future
<
dynamic
>
_never
=
Completer
<
dynamic
>().
future
;
// won't ever complete
_AsyncPassthrough
<
T
>
_verifyRouteInformationParserStillCurrent
(
Object
transaction
,
Router
<
T
>
originalWidget
)
{
return
(
T
data
)
{
if
(
transaction
==
_currentRouteInformationParserTransaction
&&
widget
.
routeInformationProvider
==
originalWidget
.
routeInformationProvider
&&
widget
.
backButtonDispatcher
==
originalWidget
.
backButtonDispatcher
&&
widget
.
routeInformationParser
==
originalWidget
.
routeInformationParser
&&
widget
.
routerDelegate
==
originalWidget
.
routerDelegate
)
{
return
SynchronousFuture
<
T
>(
data
);
}
return
_never
as
Future
<
T
>;
};
}
_AsyncPassthrough
<
void
>
_verifyRouterDelegatePushStillCurrent
(
Object
transaction
,
Router
<
T
>
originalWidget
)
{
return
(
void
data
)
{
if
(
transaction
==
_currentRouterDelegateTransaction
&&
widget
.
routeInformationProvider
==
originalWidget
.
routeInformationProvider
&&
widget
.
backButtonDispatcher
==
originalWidget
.
backButtonDispatcher
&&
widget
.
routeInformationParser
==
originalWidget
.
routeInformationParser
&&
widget
.
routerDelegate
==
originalWidget
.
routerDelegate
)
return
SynchronousFuture
<
void
>(
data
);
return
_never
;
};
}
_AsyncPassthrough
<
bool
>
_verifyRouterDelegatePopStillCurrent
(
Object
transaction
,
Router
<
T
>
originalWidget
)
{
return
(
bool
data
)
{
if
(
transaction
==
_currentRouterDelegateTransaction
&&
widget
.
routeInformationProvider
==
originalWidget
.
routeInformationProvider
&&
widget
.
backButtonDispatcher
==
originalWidget
.
backButtonDispatcher
&&
widget
.
routeInformationParser
==
originalWidget
.
routeInformationParser
&&
widget
.
routerDelegate
==
originalWidget
.
routerDelegate
)
{
return
SynchronousFuture
<
bool
>(
data
);
}
// A rebuilt was trigger from a different source. Returns true to
// prevent bubbling.
return
SynchronousFuture
<
bool
>(
true
);
};
}
Future
<
void
>
_rebuild
([
void
value
])
{
setState
(()
{
/* routerDelegate is ready to rebuild */
});
return
SynchronousFuture
<
void
>(
value
);
}
void
_handleRouterDelegateNotification
()
{
setState
(()
{
/* routerDelegate wants to rebuild */
});
_maybeNeedToReportRouteInformation
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
_RouterScope
(
routeInformationProvider:
widget
.
routeInformationProvider
,
backButtonDispatcher:
widget
.
backButtonDispatcher
,
routeInformationParser:
widget
.
routeInformationParser
,
routerDelegate:
widget
.
routerDelegate
,
routerState:
this
,
child:
Builder
(
// We use a Builder so that the build method below
// will have a BuildContext that contains the _RouterScope.
builder:
widget
.
routerDelegate
.
build
,
),
);
}
}
class
_RouterScope
extends
InheritedWidget
{
const
_RouterScope
({
Key
key
,
@required
this
.
routeInformationProvider
,
@required
this
.
backButtonDispatcher
,
@required
this
.
routeInformationParser
,
@required
this
.
routerDelegate
,
@required
this
.
routerState
,
@required
Widget
child
,
})
:
assert
(
routeInformationProvider
==
null
||
routeInformationParser
!=
null
),
assert
(
routerDelegate
!=
null
),
assert
(
routerState
!=
null
),
super
(
key:
key
,
child:
child
);
final
ValueListenable
<
RouteInformation
>
routeInformationProvider
;
final
BackButtonDispatcher
backButtonDispatcher
;
final
RouteInformationParser
<
dynamic
>
routeInformationParser
;
final
RouterDelegate
<
dynamic
>
routerDelegate
;
final
_RouterState
<
dynamic
>
routerState
;
@override
bool
updateShouldNotify
(
_RouterScope
oldWidget
)
{
return
routeInformationProvider
!=
oldWidget
.
routeInformationProvider
||
backButtonDispatcher
!=
oldWidget
.
backButtonDispatcher
||
routeInformationParser
!=
oldWidget
.
routeInformationParser
||
routerDelegate
!=
oldWidget
.
routerDelegate
||
routerState
!=
oldWidget
.
routerState
;
}
}
/// A class that can be extended or mixed in that invokes a single callback,
/// which then returns a value.
///
/// While multiple callbacks can be registered, when a notification is
/// dispatched there must be only a single callback. The return values of
/// multiple callbacks are not aggregated.
///
/// `T` is the return value expected from the callback.
///
/// See also:
///
/// * [Listenable] and its subclasses, which provide a similar mechanism for
/// one-way signalling.
class
_CallbackHookProvider
<
T
>
{
final
ObserverList
<
ValueGetter
<
T
>>
_callbacks
=
ObserverList
<
ValueGetter
<
T
>>();
/// Whether a callback is currently registered.
@protected
bool
get
hasCallbacks
=>
_callbacks
.
isNotEmpty
;
/// Register the callback to be called when the object changes.
///
/// If other callbacks have already been registered, they must be removed
/// (with [removeCallback]) before the callback is next called.
void
addCallback
(
ValueGetter
<
T
>
callback
)
=>
_callbacks
.
add
(
callback
);
/// Remove a previously registered callback.
///
/// If the given callback is not registered, the call is ignored.
void
removeCallback
(
ValueGetter
<
T
>
callback
)
=>
_callbacks
.
remove
(
callback
);
/// Calls the (single) registered callback and returns its result.
///
/// If no callback is registered, or if the callback throws, returns
/// `defaultValue`.
///
/// Call this method whenever the callback is to be invoked. If there is more
/// than one callback registered, this method will throw a [StateError].
///
/// Exceptions thrown by callbacks will be caught and reported using
/// [FlutterError.reportError].
@protected
T
invokeCallback
(
T
defaultValue
)
{
if
(
_callbacks
.
isEmpty
)
return
defaultValue
;
try
{
return
_callbacks
.
single
();
}
catch
(
exception
,
stack
)
{
FlutterError
.
reportError
(
FlutterErrorDetails
(
exception:
exception
,
stack:
stack
,
library
:
'widget library'
,
context:
ErrorDescription
(
'while invoking the callback for
$runtimeType
'
),
informationCollector:
()
sync
*
{
yield
DiagnosticsProperty
<
_CallbackHookProvider
<
T
>>(
'The
$runtimeType
that invoked the callback was:'
,
this
,
style:
DiagnosticsTreeStyle
.
errorProperty
,
);
},
));
return
defaultValue
;
}
}
}
/// Report to a [Router] when the user taps the back button on platforms that
/// support back buttons (such as Android).
///
/// When [Router] widgets are nested, consider using a
/// [ChildBackButtonDispatcher], passing it the parent [BackButtonDispatcher],
/// so that the back button requests get dispatched to the appropriate [Router].
/// To make this work properly, it's important that whenever a [Router] thinks
/// it should get the back button messages (e.g. after the user taps inside it),
/// it calls [takePriority] on its [BackButtonDispatcher] (or
/// [ChildBackButtonDispatcher]) instance.
///
/// The class takes a single callback, which must return a [Future<bool>]. The
/// callback's semantics match [WidgetsBindingObserver.didPopRoute]'s, namely,
/// the callback should return a future that completes to true if it can handle
/// the pop request, and a future that completes to false otherwise.
abstract
class
BackButtonDispatcher
extends
_CallbackHookProvider
<
Future
<
bool
>>
{
LinkedHashSet
<
ChildBackButtonDispatcher
>
_children
;
@override
bool
get
hasCallbacks
=>
super
.
hasCallbacks
||
(
_children
!=
null
&&
_children
.
isNotEmpty
);
/// Handles a pop route request.
///
/// This method prioritizes the children list in reverse order and calls
/// [ChildBackButtonDispatcher.notifiedByParent] on them. If any of them
/// handles the request (by returning a future with true), it exits this
/// method by returning this future. Otherwise, it keeps moving on to the next
/// child until a child handles the request. If none of the children handles
/// the request, this back button dispatcher will then try to handle the request
/// by itself. This back button dispatcher handles the request by notifying the
/// router which in turn calls the [RouterDelegate.popRoute] and returns its
/// result.
///
/// To decide whether this back button dispatcher will handle the pop route
/// request, you can override the [RouterDelegate.popRoute] of the router
/// delegate you pass into the router with this back button dispatcher to
/// return a future of true or false.
@override
Future
<
bool
>
invokeCallback
(
Future
<
bool
>
defaultValue
)
{
if
(
_children
!=
null
&&
_children
.
isNotEmpty
)
{
final
List
<
ChildBackButtonDispatcher
>
children
=
_children
.
toList
();
int
childIndex
=
children
.
length
-
1
;
Future
<
bool
>
notifyNextChild
(
bool
result
)
{
// If the previous child handles the callback, we returns the result.
if
(
result
)
return
SynchronousFuture
<
bool
>(
result
);
// If the previous child did not handle the callback, we ask the next
// child to handle the it.
if
(
childIndex
>
0
)
{
childIndex
-=
1
;
return
children
[
childIndex
]
.
notifiedByParent
(
defaultValue
)
.
then
<
bool
>(
notifyNextChild
);
}
// If none of the child handles the callback, the parent will then handle it.
return
super
.
invokeCallback
(
defaultValue
);
}
return
children
[
childIndex
]
.
notifiedByParent
(
defaultValue
)
.
then
<
bool
>(
notifyNextChild
);
}
return
super
.
invokeCallback
(
defaultValue
);
}
/// Creates a [ChildBackButtonDispatcher] that is a direct descendant of this
/// back button dispatcher.
///
/// To participate in handling the pop route request, call the [takePriority]
/// on the [ChildBackButtonDispatcher] created from this method.
///
/// When the pop route request is handled by this back button dispatcher, it
/// propagate the request to its direct descendants that have called the
/// [takePriority] method. If there are multiple candidates, the latest one
/// that called the [takePriority] wins the right to handle the request. If
/// the latest one does not handle the request (by returning a future of
/// false in [ChildBackButtonDispatcher.notifiedByParent]), the second latest
/// one will then have the right to handle the request. This dispatcher
/// continues finding the next candidate until there are no more candidates
/// and finally handles the request itself.
ChildBackButtonDispatcher
createChildBackButtonDispatcher
()
{
return
ChildBackButtonDispatcher
(
this
);
}
/// Make this [BackButtonDispatcher] take priority among its peers.
///
/// This has no effect when a [BackButtonDispatcher] has no parents and no
/// children. If a [BackButtonDispatcher] does have parents or children,
/// however, it causes this object to be the one to dispatch the notification
/// when the parent would normally notify its callback.
///
/// The [BackButtonDispatcher] must have a listener registered before it can
/// be told to take priority.
void
takePriority
()
{
if
(
_children
!=
null
)
_children
.
clear
();
}
/// Mark the given child as taking priority over this object and the other
/// children.
///
/// This causes [invokeCallback] to defer to the given child instead of
/// calling this object's callback.
///
/// Children are stored in a list, so that if the current child is removed
/// using [forget], a previous child will return to take its place. When
/// [takePriority] is called, the list is cleared.
///
/// Calling this again without first calling [forget] moves the child back to
/// the head of the list.
///
// (Actually it moves it to the end of the list and we treat the end of the
// list to be the priority end, but that's an implementation detail.)
//
/// The [BackButtonDispatcher] must have a listener registered before it can
/// be told to defer to a child.
void
deferTo
(
ChildBackButtonDispatcher
child
)
{
assert
(
hasCallbacks
);
_children
??=
<
ChildBackButtonDispatcher
>{}
as
LinkedHashSet
<
ChildBackButtonDispatcher
>;
_children
.
remove
(
child
);
// child may or may not be in the set already
_children
.
add
(
child
);
}
/// Causes the given child to be removed from the list of children to which
/// this object might defer, as if [deferTo] had never been called for that
/// child.
///
/// This should only be called once per child, even if [deferTo] was called
/// multiple times for that child.
///
/// If no children are left in the list, this object will stop deferring to
/// its children. (This is not the same as calling [takePriority], since, if
/// this object itself is a [ChildBackButtonDispatcher], [takePriority] would
/// additionally attempt to claim priority from its parent, whereas removing
/// the last child does not.)
void
forget
(
ChildBackButtonDispatcher
child
)
{
assert
(
_children
!=
null
);
assert
(
_children
.
contains
(
child
));
_children
.
remove
(
child
);
}
}
/// The default implementation of back button dispatcher for the root router.
///
/// This dispatcher listens to platform pop route notifications. When the
/// platform wants to pop the current route, this dispatcher calls the
/// [BackButtonDispatcher.invokeCallback] method to handle the request.
class
RootBackButtonDispatcher
extends
BackButtonDispatcher
with
WidgetsBindingObserver
{
/// Create a root back button dispatcher.
RootBackButtonDispatcher
();
@override
void
addCallback
(
ValueGetter
<
Future
<
bool
>>
callback
)
{
if
(!
hasCallbacks
)
WidgetsBinding
.
instance
.
addObserver
(
this
);
super
.
addCallback
(
callback
);
}
@override
void
removeCallback
(
ValueGetter
<
Future
<
bool
>>
callback
)
{
super
.
removeCallback
(
callback
);
if
(!
hasCallbacks
)
WidgetsBinding
.
instance
.
removeObserver
(
this
);
}
@override
Future
<
bool
>
didPopRoute
()
=>
invokeCallback
(
Future
<
bool
>.
value
(
false
));
}
/// A variant of [BackButtonDispatcher] which listens to notifications from a
/// parent back button dispatcher, and can take priority from its parent for the
/// handling of such notifications.
///
/// Useful when [Router]s are being nested within each other.
///
/// Use [Router.of] to obtain a reference to the nearest ancestor [Router], from
/// which the [Router.backButtonDispatcher] can be found, and then used as the
/// [parent] of the [ChildBackButtonDispatcher].
class
ChildBackButtonDispatcher
extends
BackButtonDispatcher
{
/// Creates a back button dispatcher that acts as the child of another.
///
/// The [parent] must not be null.
ChildBackButtonDispatcher
(
this
.
parent
)
:
assert
(
parent
!=
null
);
/// The back button dispatcher that this object will attempt to take priority
/// over when [takePriority] is called.
///
/// The parent must have a listener registered before this child object can
/// have its [takePriority] or [deferTo] methods used.
final
BackButtonDispatcher
parent
;
/// The parent of this child back button dispatcher decide to let this
/// child to handle the invoke the callback request in
/// [BackButtonDispatcher.invokeCallback].
///
/// Return a boolean future with true if this child will handle the request;
/// otherwise, return a boolean future with false.
@protected
Future
<
bool
>
notifiedByParent
(
Future
<
bool
>
defaultValue
)
{
return
invokeCallback
(
defaultValue
);
}
@override
void
takePriority
()
{
parent
.
deferTo
(
this
);
super
.
takePriority
();
}
@override
void
deferTo
(
ChildBackButtonDispatcher
child
)
{
assert
(
hasCallbacks
);
super
.
deferTo
(
child
);
}
@override
void
removeCallback
(
ValueGetter
<
Future
<
bool
>>
callback
)
{
super
.
removeCallback
(
callback
);
if
(!
hasCallbacks
)
parent
.
forget
(
this
);
}
}
/// A delegate that is used by the [Router] widget to parse a route information
/// into a configuration of type T.
///
/// This delegate is used when the [Router] widget is first built with initial
/// route information from [Router.routeInformationProvider] and any subsequent
/// new route notifications from it. The [parseRouteInformation] widget calls
/// the [parseRouteInformation] with the route information.
abstract
class
RouteInformationParser
<
T
>
{
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const
RouteInformationParser
();
/// Converts the given route information into parsed data to pass to a
/// [RouterDelegate].
///
/// The method should return a future which completes when the parsing is
/// complete. The parsing may be asynchronous if, e.g., the parser needs to
/// communicate with the OEM thread to obtain additional data about the route.
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to pass the data to the [RouterDelegate].
Future
<
T
>
parseRouteInformation
(
RouteInformation
routeInformation
);
/// Restore the route information from the given configuration.
///
/// This is not required if you do not opt for the route information reporting
/// , which is used for updating browser history for the web application. If
/// you decides to opt in, you must also overrides this method to return a
/// route information.
///
/// In practice, the [parseRouteInformation] method must produce an equivalent
/// configuration when passed this method's return value
RouteInformation
restoreRouteInformation
(
T
configuration
)
=>
null
;
}
/// A delegate that is used by the [Router] widget to build and configure a
/// navigating widget.
///
/// This delegate is the core piece of the [Router] widget. It responds to
/// push route and pop route intent from the engine and notifies the [Router]
/// to rebuild. It also act as a builder for the [Router] widget and builds a
/// navigating widget, typically a [Navigator], when the [Router] widget
/// builds.
///
/// When engine pushes a new route, the route information is parsed by the
/// [RouteInformationParser] to produce a configuration of type T. The router
/// delegate receives the configuration through [setInitialRoutePath] or
/// [setNewRoutePath] to configure itself and builds the latest navigating
/// widget upon asked.
///
/// When implementing subclass, consider defining a listenable app state to be
/// used for building the navigating widget. The router delegate should update
/// the app state accordingly and notify the listener know the app state has
/// changed when it receive route related engine intents (e.g.
/// [setNewRoutePath], [setInitialRoutePath], or [popRoute]).
///
/// All subclass must implement [setNewRoutePath], [popRoute], and [build].
///
/// See also:
///
/// * [RouteInformationParser], which is responsible for parsing the route
/// information to a configuration before passing in to router delegate.
/// * [Router], which is the widget that wires all the delegates together to
/// provide a fully functional routing solution.
abstract
class
RouterDelegate
<
T
>
extends
Listenable
{
/// Called by the [Router] at startup with the structure that the
/// [RouteInformationParser] obtained from parsing the initial route.
///
/// This should configure the [RouterDelegate] so that when [build] is
/// invoked, it will create a widget tree that matches the initial route.
///
/// By default, this method forwards the [configuration] to [setNewRoutePath].
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to schedule a build.
Future
<
void
>
setInitialRoutePath
(
T
configuration
)
{
return
setNewRoutePath
(
configuration
);
}
/// Called by the [Router] when the [Router.routeInformationProvider] reports that a
/// new route has been pushed to the application by the operating system.
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to schedule a build.
Future
<
void
>
setNewRoutePath
(
T
configuration
);
/// Called by the [Router] when the [Router.backButtonDispatcher] reports that
/// the operating system is requesting that the current route be popped.
///
/// The method should return a boolean [Future] to indicate whether this
/// delegate handles the request. Returning false will cause the entire app
/// to be popped.
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to schedule a build.
Future
<
bool
>
popRoute
();
/// Called by the [Router] when it detects a route information may have
/// changed as a result of rebuild.
///
/// If this getter returns non-null, the [Router] will start to report new
/// route information back to the engine. In web applications, the new
/// route information is used for populating browser history in order to
/// support the forward and the backward buttons.
///
/// When overriding this method, the configuration returned by this getter
/// must be able to construct the current app state and build the widget
/// with the same configuration in the [build] method if it is passed back
/// to the the [setNewRoutePath]. Otherwise, the browser backward and forward
/// buttons will not work properly.
///
/// By default, this getter returns null, which prevents the [Router] from
/// reporting the route information. To opt in, a subclass can override this
/// getter to return the current configuration.
///
/// At most one [Router] can opt in to route information reporting. Typically,
/// only the top-most [Router] created by [WidgetsApp.router] should opt for
/// route information reporting.
T
get
currentConfiguration
=>
null
;
/// Called by the [Router] to obtain the widget tree that represents the
/// current state.
///
/// This is called whenever the [setInitialRoutePath] method's future
/// completes, the [setNewRoutePath] method's future completes with the value
/// true, the [popRoute] method's future completes with the value true, or
/// this object notifies its clients (see the [Listenable] interface, which
/// this interface includes). In addition, it may be called at other times. It
/// is important, therefore, that the methods above do not update the state
/// that the [build] method uses before they complete their respective
/// futures.
///
/// Typically this method returns a suitably-configured [Navigator]. If you do
/// plan to create a navigator, consider using the
/// [PopNavigatorRouterDelegateMixin].
///
/// This method must not return null.
///
/// The `context` is the [Router]'s build context.
Widget
build
(
BuildContext
context
);
}
/// A route information provider that provides route information for the
/// [Router] widget
///
/// This provider is responsible for handing the route information through [value]
/// getter and notifies listeners, typically the [Router] widget, when a new
/// route information is available.
///
/// When the router opts for the route information reporting (by overrides the
/// [RouterDelegate.currentConfiguration] to return non-null), overrides the
/// [routerReportsNewRouteInformation] method to process the route information.
///
/// See also:
///
/// * [PlatformRouteInformationProvider], which wires up the itself with the
/// [WidgetsBindingObserver.didPushRoute] to propagate platform push route
/// intent to the [Router] widget, as well as reports new route information
/// from the [Router] back to the engine by overriding the
/// [routerReportsNewRouteInformation].
abstract
class
RouteInformationProvider
extends
ValueListenable
<
RouteInformation
>
{
/// A callback called when the [Router] widget detects any navigation event
/// due to state changes.
///
/// The subclasses can override this method to update theirs values or trigger
/// other side effects. For example, the [PlatformRouteInformationProvider]
/// overrides this method to report the route information back to the engine.
///
/// The [routeInformation] is the new route information after the navigation
/// event.
void
routerReportsNewRouteInformation
(
RouteInformation
routeInformation
)
{}
}
/// The route information provider that propagates the platform route information changes.
///
/// This provider also reports the new route information from the [Router] widget
/// back to engine using message channel method, the
/// [SystemNavigator.routeInformationUpdated].
class
PlatformRouteInformationProvider
extends
RouteInformationProvider
with
WidgetsBindingObserver
,
ChangeNotifier
{
/// Create a platform route information provider.
///
/// Use the [initialRouteInformation] to set the default route information for this
/// provider.
PlatformRouteInformationProvider
({
RouteInformation
initialRouteInformation
})
:
_value
=
initialRouteInformation
;
@override
void
routerReportsNewRouteInformation
(
RouteInformation
routeInformation
)
{
SystemNavigator
.
routeInformationUpdated
(
location:
routeInformation
.
location
,
state:
routeInformation
.
state
,
);
_value
=
routeInformation
;
}
@override
RouteInformation
get
value
=>
_value
;
RouteInformation
_value
;
void
_platformReportsNewRouteInformation
(
RouteInformation
routeInformation
)
{
if
(
_value
==
routeInformation
)
return
;
_value
=
routeInformation
;
notifyListeners
();
}
@override
void
addListener
(
VoidCallback
listener
)
{
if
(!
hasListeners
)
WidgetsBinding
.
instance
.
addObserver
(
this
);
super
.
addListener
(
listener
);
}
@override
void
removeListener
(
VoidCallback
listener
)
{
super
.
removeListener
(
listener
);
if
(!
hasListeners
)
WidgetsBinding
.
instance
.
removeObserver
(
this
);
}
@override
void
dispose
()
{
// In practice, this will rarely be called. We assume that the listeners
// will be added and removed in a coherent fashion such that when the object
// is no longer being used, there's no listener, and so it will get garbage
// collected.
if
(
hasListeners
)
WidgetsBinding
.
instance
.
removeObserver
(
this
);
super
.
dispose
();
}
@override
Future
<
bool
>
didPushRouteInformation
(
RouteInformation
routeInformation
)
async
{
assert
(
hasListeners
);
_platformReportsNewRouteInformation
(
routeInformation
);
return
true
;
}
@override
Future
<
bool
>
didPushRoute
(
String
route
)
async
{
assert
(
hasListeners
);
_platformReportsNewRouteInformation
(
RouteInformation
(
location:
route
));
return
true
;
}
}
/// A mixin that wires [RouterDelegate.popRoute] to the [Navigator] it builds.
///
/// This mixin calls [Navigator.maybePop] when it receives an Android back
/// button intent through the [RouterDelegate.popRoute]. Using this mixin
/// guarantees that the back button still respects pageless routes in the
/// navigator.
///
/// Only use this mixin if you plan to build a navigator in the
/// [RouterDelegate.build].
mixin
PopNavigatorRouterDelegateMixin
<
T
>
on
RouterDelegate
<
T
>
{
/// The key used for retrieving the current navigator.
///
/// When using this mixin, be sure to use this key to create the navigator.
GlobalKey
<
NavigatorState
>
get
navigatorKey
;
@override
Future
<
bool
>
popRoute
()
{
final
NavigatorState
navigator
=
navigatorKey
?.
currentState
;
if
(
navigator
==
null
)
return
SynchronousFuture
<
bool
>(
false
);
return
navigator
.
maybePop
();
}
}
packages/flutter/lib/widgets.dart
View file @
f9fd71bc
...
...
@@ -85,6 +85,7 @@ export 'src/widgets/primary_scroll_controller.dart';
export
'src/widgets/raw_keyboard_listener.dart'
;
export
'src/widgets/restoration.dart'
;
export
'src/widgets/restoration_properties.dart'
;
export
'src/widgets/router.dart'
;
export
'src/widgets/routes.dart'
;
export
'src/widgets/safe_area.dart'
;
export
'src/widgets/scroll_activity.dart'
;
...
...
packages/flutter/test/cupertino/app_test.dart
View file @
f9fd71bc
...
...
@@ -6,6 +6,8 @@
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/services.dart'
;
void
main
(
)
{
testWidgets
(
'Heroes work'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -147,4 +149,101 @@ void main() {
expect
(
key2
.
currentState
,
isA
<
NavigatorState
>());
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
),
)
],
);
}
}
packages/flutter/test/material/app_test.dart
View file @
f9fd71bc
...
...
@@ -4,7 +4,9 @@
// @dart = 2.8
import
'package:flutter/foundation.dart'
;
import
'package:flutter/semantics.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/material.dart'
;
...
...
@@ -947,6 +949,37 @@ void main() {
expect
(
key2
.
currentState
,
isA
<
NavigatorState
>());
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
{
...
...
@@ -968,3 +1001,69 @@ class MockAccessibilityFeature implements AccessibilityFeatures {
@override
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
),
)
],
);
}
}
packages/flutter/test/widgets/app_test.dart
View file @
f9fd71bc
...
...
@@ -264,4 +264,116 @@ void main() {
expect
(
find
.
text
(
'non-regular page one'
),
findsOneWidget
);
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
),
)
],
);
}
}
packages/flutter/test/widgets/binding_test.dart
View file @
f9fd71bc
...
...
@@ -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
(
)
{
setUp
(()
{
WidgetsFlutterBinding
.
ensureInitialized
();
...
...
@@ -90,6 +100,38 @@ void main() {
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
{
final
BinaryMessenger
defaultBinaryMessenger
=
ServicesBinding
.
instance
.
defaultBinaryMessenger
;
ByteData
message
;
...
...
packages/flutter/test/widgets/route_notification_messages_test.dart
View file @
f9fd71bc
...
...
@@ -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
(
)
{
testWidgets
(
'Push and Pop should send platform messages'
,
(
WidgetTester
tester
)
async
{
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
);
}
packages/flutter/test/widgets/router_test.dart
0 → 100644
View file @
f9fd71bc
// 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
);
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment