Provide a way to override global InheritedWidgets (#14348)

For example, so that the gallery can override the media query globally.
This commit is contained in:
Ian Hickson 2018-02-01 23:12:52 -08:00 committed by GitHub
parent 12ceaefb18
commit c12d120bbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 603 additions and 281 deletions

View File

@ -51,6 +51,7 @@ class GalleryAppState extends State<GalleryApp> {
bool _showPerformanceOverlay = false;
bool _checkerboardRasterCacheImages = false;
bool _checkerboardOffscreenLayers = false;
TextDirection _overrideDirection = TextDirection.ltr;
double _timeDilation = 1.0;
TargetPlatform _platform;
@ -139,6 +140,12 @@ class GalleryAppState extends State<GalleryApp> {
_textScaleFactor = value;
});
},
overrideDirection: _overrideDirection,
onOverrideDirectionChanged: (TextDirection value) {
setState(() {
_overrideDirection = value;
});
},
onSendFeedback: widget.onSendFeedback,
);
@ -155,7 +162,7 @@ class GalleryAppState extends State<GalleryApp> {
// using named routes, consider the example in the Navigator class documentation:
// https://docs.flutter.io/flutter/widgets/Navigator-class.html
_kRoutes[item.routeName] = (BuildContext context) {
return _applyScaleFactor(item.buildRoute(context));
return item.buildRoute(context);
};
}
@ -168,6 +175,12 @@ class GalleryAppState extends State<GalleryApp> {
checkerboardOffscreenLayers: _checkerboardOffscreenLayers,
routes: _kRoutes,
home: _applyScaleFactor(home),
builder: (BuildContext context, Widget child) {
return new Directionality(
textDirection: _overrideDirection,
child: _applyScaleFactor(child),
);
},
);
}
}

View File

@ -120,6 +120,8 @@ class GalleryDrawer extends StatelessWidget {
this.checkerboardOffscreenLayers,
this.onCheckerboardOffscreenLayersChanged,
this.onPlatformChanged,
this.overrideDirection: TextDirection.ltr,
this.onOverrideDirectionChanged,
this.onSendFeedback,
}) : assert(onThemeChanged != null),
assert(onTimeDilationChanged != null),
@ -145,6 +147,9 @@ class GalleryDrawer extends StatelessWidget {
final ValueChanged<TargetPlatform> onPlatformChanged;
final TextDirection overrideDirection;
final ValueChanged<TextDirection> onOverrideDirectionChanged;
final VoidCallback onSendFeedback;
@override
@ -220,6 +225,16 @@ class GalleryDrawer extends StatelessWidget {
selected: timeDilation != 1.0,
);
final Widget overrideDirectionItem = new CheckboxListTile(
title: const Text('Force RTL'),
value: overrideDirection == TextDirection.rtl,
onChanged: (bool value) {
onOverrideDirectionChanged(value ? TextDirection.rtl : TextDirection.ltr);
},
secondary: const Icon(Icons.format_textdirection_r_to_l),
selected: overrideDirection == TextDirection.rtl,
);
final Widget sendFeedbackItem = new ListTile(
leading: const Icon(Icons.report),
title: const Text('Send feedback'),
@ -285,6 +300,7 @@ class GalleryDrawer extends StatelessWidget {
allDrawerItems.addAll(textSizeItems);
allDrawerItems..addAll(<Widget>[
overrideDirectionItem,
const Divider(),
animateSlowlyItem,
const Divider(),

View File

@ -77,6 +77,8 @@ class GalleryHome extends StatefulWidget {
this.checkerboardOffscreenLayers,
this.onCheckerboardOffscreenLayersChanged,
this.onPlatformChanged,
this.overrideDirection: TextDirection.ltr,
this.onOverrideDirectionChanged,
this.onSendFeedback,
}) : assert(onThemeChanged != null),
assert(onTimeDilationChanged != null),
@ -102,6 +104,9 @@ class GalleryHome extends StatefulWidget {
final ValueChanged<TargetPlatform> onPlatformChanged;
final TextDirection overrideDirection;
final ValueChanged<TextDirection> onOverrideDirectionChanged;
final VoidCallback onSendFeedback;
@override
@ -177,6 +182,8 @@ class GalleryHomeState extends State<GalleryHome> with SingleTickerProviderState
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
onCheckerboardOffscreenLayersChanged: widget.onCheckerboardOffscreenLayersChanged,
onPlatformChanged: widget.onPlatformChanged,
overrideDirection: widget.overrideDirection,
onOverrideDirectionChanged: widget.onOverrideDirectionChanged,
onSendFeedback: widget.onSendFeedback,
),
body: new CustomScrollView(

View File

@ -69,7 +69,7 @@ void main() {
expect(newTextSize, equals(origTextSize));
// Scroll to the bottom of the menu.
await tester.drag(find.text('Small'), const Offset(0.0, -450.0));
await tester.drag(find.text('Small'), const Offset(0.0, -1000.0));
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // Wait until it's changed.

View File

@ -159,7 +159,7 @@ Future<Null> runSmokeTest(WidgetTester tester) async {
await tester.pump(const Duration(seconds: 1)); // Wait until it's changed.
// Scroll the 'Send feedback' item into view.
await tester.drag(find.text('Small'), const Offset(0.0, -450.0));
await tester.drag(find.text('Small'), const Offset(0.0, -1000.0));
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // Wait until it's changed.

View File

@ -51,13 +51,16 @@ const TextStyle _errorTextStyle = const TextStyle(
///
/// 4. Finally if all else fails [onUnknownRoute] is called.
///
/// At least one of these options must handle the `/` route, since it is used
/// when an invalid [initialRoute] is specified on startup (e.g. by another
/// application launching this one with an intent on Android; see
/// [Window.defaultRouteName]).
/// If a [Navigator] is created, at least one of these options must handle the
/// `/` route, since it is used when an invalid [initialRoute] is specified on
/// startup (e.g. by another application launching this one with an intent on
/// Android; see [Window.defaultRouteName]).
///
/// This widget also configures the top-level [Navigator]'s observer to perform
/// [Hero] animations.
/// This widget also configures the observer of the top-level [Navigator] (if
/// any) to perform [Hero] animations.
///
/// If [home], [routes], [onGenerateRoute], and [onUnknownRoute] are all null,
/// and [builder] is not null, then no [Navigator] is created.
///
/// See also:
///
@ -68,8 +71,8 @@ const TextStyle _errorTextStyle = const TextStyle(
class MaterialApp extends StatefulWidget {
/// Creates a MaterialApp.
///
/// At least one of [home], [routes], or [onGenerateRoute] must be given. If
/// only [routes] is given, it must include an entry for the
/// At least one of [home], [routes], [onGenerateRoute], or [builder] must be
/// non-null. If only [routes] is given, it must include an entry for the
/// [Navigator.defaultRouteName] (`/`), since that is the route used when the
/// application is launched with an intent that specifies an otherwise
/// unsupported route.
@ -80,35 +83,29 @@ class MaterialApp extends StatefulWidget {
MaterialApp({ // can't be const because the asserts use methods on Map :-(
Key key,
this.navigatorKey,
this.title: '',
this.onGenerateTitle,
this.color,
this.theme,
this.home,
this.routes: const <String, WidgetBuilder>{},
this.initialRoute,
this.onGenerateRoute,
this.onUnknownRoute,
this.navigatorObservers: const <NavigatorObserver>[],
this.builder,
this.title: '',
this.onGenerateTitle,
this.color,
this.theme,
this.locale,
this.localizationsDelegates,
this.localeResolutionCallback,
this.supportedLocales: const <Locale>[const Locale('en', 'US')],
this.navigatorObservers: const <NavigatorObserver>[],
this.debugShowMaterialGrid: false,
this.showPerformanceOverlay: false,
this.checkerboardRasterCacheImages: false,
this.checkerboardOffscreenLayers: false,
this.showSemanticsDebugger: false,
this.debugShowCheckedModeBanner: true
}) : assert(title != null),
assert(routes != null),
this.debugShowCheckedModeBanner: true,
}) : assert(routes != null),
assert(navigatorObservers != null),
assert(debugShowMaterialGrid != null),
assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null),
assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null),
assert(
home == null ||
!routes.containsKey(Navigator.defaultRouteName),
@ -116,6 +113,7 @@ class MaterialApp extends StatefulWidget {
'cannot include an entry for "/", since it would be redundant.'
),
assert(
builder != null ||
home != null ||
routes.containsKey(Navigator.defaultRouteName) ||
onGenerateRoute != null ||
@ -124,9 +122,35 @@ class MaterialApp extends StatefulWidget {
'or the routes table must include an entry for "/", '
'or there must be on onGenerateRoute callback specified, '
'or there must be an onUnknownRoute callback specified, '
'or the builder property must be specified, '
'because otherwise there is nothing to fall back on if the '
'app is started with an intent that specifies an unknown route.'
),
assert(
(home != null ||
routes.isNotEmpty ||
onGenerateRoute != null ||
onUnknownRoute != null)
||
(builder != null &&
navigatorKey == null &&
initialRoute == null &&
navigatorObservers.isEmpty),
'If no route is provided using '
'home, routes, onGenerateRoute, or onUnknownRoute, '
'a non-null callback for the builder property must be provided, '
'and the other navigator-related properties, '
'navigatorKey, initialRoute, and navigatorObservers, '
'must have their initial values '
'(null, null, and the empty list, respectively).'
),
assert(title != null),
assert(debugShowMaterialGrid != null),
assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null),
assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null),
super(key: key);
/// A key to use when building the [Navigator].
@ -140,8 +164,173 @@ class MaterialApp extends StatefulWidget {
/// application state in the process; in that case, the [navigatorObservers]
/// must also be changed, since the previous observers will be attached to the
/// previous navigator.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [navigatorKey] must be null and [builder] must not be null.
final GlobalKey<NavigatorState> navigatorKey;
/// The widget for the default route of the app ([Navigator.defaultRouteName],
/// which is `/`).
///
/// This is the route that is displayed first when the application is started
/// normally, unless [initialRoute] is specified. It's also the route that's
/// displayed if the [initialRoute] can't be displayed.
///
/// To be able to directly call [Theme.of], [MediaQuery.of], etc, in the code
/// that sets the [home] argument in the constructor, you can use a [Builder]
/// widget to get a [BuildContext].
///
/// If [home] is specified, then [routes] must not include an entry for `/`,
/// as [home] takes its place.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [builder] must not be null.
///
/// The difference between using [home] and using [builder] is that the [home]
/// subtree is inserted into the application below a [Navigator] (and thus
/// below an [Overlay], which [Navigator] uses). With [home], therefore,
/// dialog boxes will work automatically, [Tooltip]s will work, the [routes]
/// table will be used, and APIs such as [Navigator.push] and [Navigator.pop]
/// will work as expected. In contrast, the widget returned from [builder] is
/// inserted _above_ the [MaterialApp]'s [Navigator] (if any).
final Widget home;
/// The application's top-level routing table.
///
/// When a named route is pushed with [Navigator.pushNamed], the route name is
/// looked up in this map. If the name is present, the associated
/// [WidgetBuilder] is used to construct a [MaterialPageRoute] that performs
/// an appropriate transition, including [Hero] animations, to the new route.
///
/// If the app only has one page, then you can specify it using [home] instead.
///
/// If [home] is specified, then it implies an entry in this table for the
/// [Navigator.defaultRouteName] route (`/`), and it is an error to
/// redundantly provide such a route in the [routes] table.
///
/// If a route is requested that is not specified in this table (or by
/// [home]), then the [onGenerateRoute] callback is called to build the page
/// instead.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [builder] must not be null.
final Map<String, WidgetBuilder> routes;
/// The name of the first route to show, if a [Navigator] is built.
///
/// Defaults to [Window.defaultRouteName], which may be overridden by the code
/// that launched the application.
///
/// If the route contains slashes, then it is treated as a "deep link", and
/// before this route is pushed, the routes leading to this one are pushed
/// also. For example, if the route was `/a/b/c`, then the app would start
/// with the three routes `/a`, `/a/b`, and `/a/b/c` loaded, in that order.
///
/// If any part of this process fails to generate routes, then the
/// [initialRoute] is ignored and [Navigator.defaultRouteName] is used instead
/// (`/`). This can happen if the app is started with an intent that specifies
/// a non-existent route.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [initialRoute] must be null and [builder] must not be null.
///
/// See also:
///
/// * [Navigator.initialRoute], which is used to implement this property.
/// * [Navigator.push], for pushing additional routes.
/// * [Navigator.pop], for removing a route from the stack.
final String initialRoute;
/// The route generator callback used when the app is navigated to a
/// named route.
///
/// This is used if [routes] does not contain the requested route.
///
/// If this returns null when building the routes to handle the specified
/// [initialRoute], then all the routes are discarded and
/// [Navigator.defaultRouteName] is used instead (`/`). See [initialRoute].
///
/// During normal app operation, the [onGenerateRoute] callback will only be
/// applied to route names pushed by the application, and so should never
/// return null.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [builder] must not be null.
final RouteFactory onGenerateRoute;
/// Called when [onGenerateRoute] fails to generate a route, except for the
/// [initialRoute].
///
/// This callback is typically used for error handling. For example, this
/// callback might always generate a "not found" page that describes the route
/// that wasn't found.
///
/// The default implementation pushes a route that displays an ugly error
/// message.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [builder] must not be null.
final RouteFactory onUnknownRoute;
/// The list of observers for the [Navigator] created for this app.
///
/// This list must be replaced by a list of newly-created observers if the
/// [navigatorKey] is changed.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [navigatorObservers] must be the empty list and [builder] must not be null.
final List<NavigatorObserver> navigatorObservers;
/// A builder for inserting widgets above the [Navigator] but below the other
/// widgets created by the [MaterialApp] widget, or for replacing the
/// [Navigator] entirely.
///
/// For example, from the [BuildContext] passed to this method, the
/// [Directionality], [Localizations], [DefaultTextStyle], [MediaQuery], etc,
/// are all available. They can also be overridden in a way that impacts all
/// the routes in the [Navigator].
///
/// This is rarely useful, but can be used in applications that wish to
/// override those defaults, e.g. to force the application into right-to-left
/// mode despite being in English, or to override the [MediaQuery] metrics
/// (e.g. to leave a gap for advertisements shown by a plugin from OEM code).
///
/// The [builder] callback is passed two arguments, the [BuildContext] (as
/// `context`) and a [Navigator] widget (as `child`).
///
/// If no routes are provided using [home], [routes], [onGenerateRoute], or
/// [onUnknownRoute], the `child` will be null, and it is the responsibility
/// of the [builder] to provide the application's routing machinery.
///
/// If routes _are_ provided using one or more of those properties, then
/// `child` is not null, and the returned value should include the `child` in
/// the widget subtree; if it does not, then the application will have no
/// navigator and the [navigatorKey], [home], [routes], [onGenerateRoute],
/// [onUnknownRoute], [initialRoute], and [navigatorObservers] properties will
/// have no effect.
///
/// If [builder] is null, it is as if a builder was specified that returned
/// the `child` directly. If it is null, routes must be provided using one of
/// the other properties listed above.
///
/// Unless a [Navigator] is provided, either implicitly from [builder] being
/// null, or by a [builder] including its `child` argument, or by a [builder]
/// explicitly providing a [Navigator] of its own, features such as
/// [showDialog] and [showMenu], widgets such as [Tooltip], [PopupMenuButton],
/// or [Hero], and APIs such as [Navigator.push] and [Navigator.pop], will not
/// function.
///
/// For specifically overriding the [title] with a value based on the
/// [Localizations], consider [onGenerateTitle] instead.
final TransitionBuilder builder;
/// A one-line description used by the device to identify the app for the user.
///
/// On Android the titles appear above the task manager's app snapshots which are
@ -172,21 +361,6 @@ class MaterialApp extends StatefulWidget {
/// The colors to use for the application's widgets.
final ThemeData theme;
/// The widget for the default route of the app ([Navigator.defaultRouteName],
/// which is `/`).
///
/// This is the route that is displayed first when the application is started
/// normally, unless [initialRoute] is specified. It's also the route that's
/// displayed if the [initialRoute] can't be displayed.
///
/// To be able to directly call [Theme.of], [MediaQuery.of], etc, in the code
/// that sets the [home] argument in the constructor, you can use a [Builder]
/// widget to get a [BuildContext].
///
/// If [home] is specified, then [routes] must not include an entry for `/`,
/// as [home] takes its place.
final Widget home;
/// The primary color to use for the application in the operating system
/// interface.
///
@ -194,71 +368,6 @@ class MaterialApp extends StatefulWidget {
/// application switcher.
final Color color;
/// The application's top-level routing table.
///
/// When a named route is pushed with [Navigator.pushNamed], the route name is
/// looked up in this map. If the name is present, the associated
/// [WidgetBuilder] is used to construct a [MaterialPageRoute] that performs
/// an appropriate transition, including [Hero] animations, to the new route.
///
/// If the app only has one page, then you can specify it using [home] instead.
///
/// If [home] is specified, then it implies an entry in this table for the
/// [Navigator.defaultRouteName] route (`/`), and it is an error to
/// redundantly provide such a route in the [routes] table.
///
/// If a route is requested that is not specified in this table (or by
/// [home]), then the [onGenerateRoute] callback is called to build the page
/// instead.
final Map<String, WidgetBuilder> routes;
/// The name of the first route to show.
///
/// Defaults to [Window.defaultRouteName], which may be overridden by the code
/// that launched the application.
///
/// If the route contains slashes, then it is treated as a "deep link", and
/// before this route is pushed, the routes leading to this one are pushed
/// also. For example, if the route was `/a/b/c`, then the app would start
/// with the three routes `/a`, `/a/b`, and `/a/b/c` loaded, in that order.
///
/// If any part of this process fails to generate routes, then the
/// [initialRoute] is ignored and [Navigator.defaultRouteName] is used instead
/// (`/`). This can happen if the app is started with an intent that specifies
/// a non-existent route.
///
/// See also:
///
/// * [Navigator.initialRoute], which is used to implement this property.
/// * [Navigator.push], for pushing additional routes.
/// * [Navigator.pop], for removing a route from the stack.
final String initialRoute;
/// The route generator callback used when the app is navigated to a
/// named route.
///
/// This is used if [routes] does not contain the requested route.
///
/// If this returns null when building the routes to handle the specified
/// [initialRoute], then all the routes are discarded and
/// [Navigator.defaultRouteName] is used instead (`/`). See [initialRoute].
///
/// During normal app operation, the [onGenerateRoute] callback will only be
/// applied to route names pushed by the application, and so should never
/// return null.
final RouteFactory onGenerateRoute;
/// Called when [onGenerateRoute] fails to generate a route, except for the
/// [initialRoute].
///
/// This callback is typically used for error handling. For example, this
/// callback might always generate a "not found" page that describes the route
/// that wasn't found.
///
/// The default implementation pushes a route that displays an ugly error
/// message.
final RouteFactory onUnknownRoute;
/// The initial locale for this app's [Localizations] widget.
///
/// If the `locale` is null the system's locale value is used.
@ -417,12 +526,6 @@ class MaterialApp extends StatefulWidget {
/// representative of what will happen in release mode.
final bool debugShowCheckedModeBanner;
/// The list of observers for the [Navigator] created for this app.
///
/// This list must be replaced by a list of newly-created observers if the
/// [navigatorKey] is changed.
final List<NavigatorObserver> navigatorObservers;
/// Turns on a [GridPaper] overlay that paints a baseline grid
/// Material apps.
///
@ -469,6 +572,7 @@ class _MaterialAppState extends State<MaterialApp> {
void initState() {
super.initState();
_heroController = new HeroController(createRectTween: _createRectTween);
_updateNavigator();
}
@override
@ -481,17 +585,19 @@ class _MaterialAppState extends State<MaterialApp> {
// Navigator has a GlobalKey).
_heroController = new HeroController(createRectTween: _createRectTween);
}
_updateNavigator();
}
// Combine the Localizations for Material with the ones contributed
// by the localizationsDelegates parameter, if any. Only the first delegate
// of a particular LocalizationsDelegate.type is loaded so the
// localizationsDelegate parameter can be used to override
// _MaterialLocalizationsDelegate.
Iterable<LocalizationsDelegate<dynamic>> get _localizationsDelegates sync* {
if (widget.localizationsDelegates != null)
yield* widget.localizationsDelegates;
yield DefaultMaterialLocalizations.delegate;
bool _haveNavigator;
List<NavigatorObserver> _navigatorObservers;
void _updateNavigator() {
_haveNavigator = widget.home != null ||
widget.routes.isNotEmpty ||
widget.onGenerateRoute != null ||
widget.onUnknownRoute != null;
_navigatorObservers = new List<NavigatorObserver>.from(widget.navigatorObservers)
..add(_heroController);
}
RectTween _createRectTween(Rect begin, Rect end) {
@ -548,6 +654,17 @@ class _MaterialAppState extends State<MaterialApp> {
return result;
}
// Combine the Localizations for Material with the ones contributed
// by the localizationsDelegates parameter, if any. Only the first delegate
// of a particular LocalizationsDelegate.type is loaded so the
// localizationsDelegate parameter can be used to override
// _MaterialLocalizationsDelegate.
Iterable<LocalizationsDelegate<dynamic>> get _localizationsDelegates sync* {
if (widget.localizationsDelegates != null)
yield* widget.localizationsDelegates;
yield DefaultMaterialLocalizations.delegate;
}
@override
Widget build(BuildContext context) {
final ThemeData theme = widget.theme ?? new ThemeData.fallback();
@ -557,17 +674,16 @@ class _MaterialAppState extends State<MaterialApp> {
child: new WidgetsApp(
key: new GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: _haveNavigator ? _navigatorObservers : null,
initialRoute: widget.initialRoute,
onGenerateRoute: _haveNavigator ? _onGenerateRoute : null,
onUnknownRoute: _haveNavigator ? _onUnknownRoute : null,
builder: widget.builder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: _errorTextStyle,
// blue is the primary color of the default theme
color: widget.color ?? theme?.primaryColor ?? Colors.blue,
navigatorObservers:
new List<NavigatorObserver>.from(widget.navigatorObservers)
..add(_heroController),
initialRoute: widget.initialRoute,
onGenerateRoute: _onGenerateRoute,
onUnknownRoute: _onUnknownRoute,
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,

View File

@ -53,29 +53,33 @@ typedef String GenerateAppTitle(BuildContext context);
/// See also: [CheckedModeBanner], [DefaultTextStyle], [MediaQuery],
/// [Localizations], [Title], [Navigator], [Overlay], [SemanticsDebugger] (the
/// widgets wrapped by this one).
///
/// The [onGenerateRoute] argument is required, and corresponds to
/// [Navigator.onGenerateRoute].
class WidgetsApp extends StatefulWidget {
/// Creates a widget that wraps a number of widgets that are commonly
/// required for an application.
///
/// The boolean arguments, [color], [navigatorObservers], and
/// [onGenerateRoute] must not be null.
/// The boolean arguments, [color], and [navigatorObservers] must not be null.
///
/// If the [builder] is null, the [onGenerateRoute] argument is required, and
/// corresponds to [Navigator.onGenerateRoute]. If the [builder] is non-null
/// and the [onGenerateRoute] argument is null, then the [builder] will not be
/// provided with a [Navigator]. If [onGenerateRoute] is not provided,
/// [navigatorKey], [onUnknownRoute], [navigatorObservers], and [initialRoute]
/// must have their default values, as they will have no effect.
///
/// The `supportedLocales` argument must be a list of one or more elements.
/// By default supportedLocales is `[const Locale('en', 'US')]`.
WidgetsApp({ // can't be const because the asserts use methods on Iterable :-(
Key key,
this.navigatorKey,
@required this.onGenerateRoute,
this.onGenerateRoute,
this.onUnknownRoute,
this.navigatorObservers: const <NavigatorObserver>[],
this.initialRoute,
this.builder,
this.title: '',
this.onGenerateTitle,
this.textStyle,
@required this.color,
this.navigatorObservers: const <NavigatorObserver>[],
this.initialRoute,
this.locale,
this.localizationsDelegates,
this.localeResolutionCallback,
@ -87,10 +91,14 @@ class WidgetsApp extends StatefulWidget {
this.debugShowWidgetInspector: false,
this.debugShowCheckedModeBanner: true,
this.inspectorSelectButtonBuilder,
}) : assert(title != null),
assert(onGenerateRoute != null),
}) : assert(navigatorObservers != null),
assert(onGenerateRoute != null || navigatorKey == null),
assert(onGenerateRoute != null || onUnknownRoute == null),
assert(onGenerateRoute != null || navigatorObservers == const <NavigatorObserver>[]),
assert(onGenerateRoute != null || initialRoute == null),
assert(onGenerateRoute != null || builder != null),
assert(title != null),
assert(color != null),
assert(navigatorObservers != null),
assert(supportedLocales != null && supportedLocales.isNotEmpty),
assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null),
@ -111,8 +119,109 @@ class WidgetsApp extends StatefulWidget {
/// application state in the process; in that case, the [navigatorObservers]
/// must also be changed, since the previous observers will be attached to the
/// previous navigator.
///
/// The [Navigator] is only built if [onGenerateRoute] is not null; if it is
/// null, [navigatorKey] must also be null.
final GlobalKey<NavigatorState> navigatorKey;
/// The route generator callback used when the app is navigated to a
/// named route.
///
/// If this returns null when building the routes to handle the specified
/// [initialRoute], then all the routes are discarded and
/// [Navigator.defaultRouteName] is used instead (`/`). See [initialRoute].
///
/// During normal app operation, the [onGenerateRoute] callback will only be
/// applied to route names pushed by the application, and so should never
/// return null.
///
/// The [Navigator] is only built if [onGenerateRoute] is not null. If
/// [onGenerateRoute] is null, the [builder] must be non-null.
final RouteFactory onGenerateRoute;
/// Called when [onGenerateRoute] fails to generate a route.
///
/// This callback is typically used for error handling. For example, this
/// callback might always generate a "not found" page that describes the route
/// that wasn't found.
///
/// Unknown routes can arise either from errors in the app or from external
/// requests to push routes, such as from Android intents.
///
/// The [Navigator] is only built if [onGenerateRoute] is not null; if it is
/// null, [onUnknownRoute] must also be null.
final RouteFactory onUnknownRoute;
/// The name of the first route to show.
///
/// Defaults to [Window.defaultRouteName], which may be overridden by the code
/// that launched the application.
///
/// If the route contains slashes, then it is treated as a "deep link", and
/// before this route is pushed, the routes leading to this one are pushed
/// also. For example, if the route was `/a/b/c`, then the app would start
/// with the three routes `/a`, `/a/b`, and `/a/b/c` loaded, in that order.
///
/// If any part of this process fails to generate routes, then the
/// [initialRoute] is ignored and [Navigator.defaultRouteName] is used instead
/// (`/`). This can happen if the app is started with an intent that specifies
/// a non-existent route.
///
/// The [Navigator] is only built if [onGenerateRoute] is not null; if it is
/// null, [initialRoute] must also be null.
///
/// See also:
///
/// * [Navigator.initialRoute], which is used to implement this property.
/// * [Navigator.push], for pushing additional routes.
/// * [Navigator.pop], for removing a route from the stack.
final String initialRoute;
/// The list of observers for the [Navigator] created for this app.
///
/// This list must be replaced by a list of newly-created observers if the
/// [navigatorKey] is changed.
///
/// The [Navigator] is only built if [onGenerateRoute] is not null; if it is
/// null, [navigatorObservers] must be left to its default value, the empty
/// list.
final List<NavigatorObserver> navigatorObservers;
/// A builder for inserting widgets above the [Navigator] but below the other
/// widgets created by the [WidgetsApp] widget, or for replacing the
/// [Navigator] entirely.
///
/// For example, from the [BuildContext] passed to this method, the
/// [Directionality], [Localizations], [DefaultTextStyle], [MediaQuery], etc,
/// are all available. They can also be overridden in a way that impacts all
/// the routes in the [Navigator].
///
/// This is rarely useful, but can be used in applications that wish to
/// override those defaults, e.g. to force the application into right-to-left
/// mode despite being in English, or to override the [MediaQuery] metrics
/// (e.g. to leave a gap for advertisements shown by a plugin from OEM code).
///
/// The [builder] callback is passed two arguments, the [BuildContext] (as
/// `context`) and a [Navigator] widget (as `child`).
///
/// If [onGenerateRoute] is null, the `child` will be null, and it is the
/// responsibility of the [builder] to provide the application's routing
/// machinery.
///
/// If [onGenerateRoute] is not null, then `child` is not null, and the
/// returned value should include the `child` in the widget subtree; if it
/// does not, then the application will have no navigator and the
/// [navigatorKey], [onGenerateRoute], [onUnknownRoute], [initialRoute], and
/// [navigatorObservers] properties will have no effect.
///
/// If [builder] is null, it is as if a builder was specified that returned
/// the `child` directly. At least one of either [onGenerateRoute] or
/// [builder] must be non-null.
///
/// For specifically overriding the [title] with a value based on the
/// [Localizations], consider [onGenerateTitle] instead.
final TransitionBuilder builder;
/// A one-line description used by the device to identify the app for the user.
///
/// On Android the titles appear above the task manager's app snapshots which are
@ -146,50 +255,6 @@ class WidgetsApp extends StatefulWidget {
/// application switcher.
final Color color;
/// The route generator callback used when the app is navigated to a
/// named route.
///
/// If this returns null when building the routes to handle the specified
/// [initialRoute], then all the routes are discarded and
/// [Navigator.defaultRouteName] is used instead (`/`). See [initialRoute].
///
/// During normal app operation, the [onGenerateRoute] callback will only be
/// applied to route names pushed by the application, and so should never
/// return null.
final RouteFactory onGenerateRoute;
/// Called when [onGenerateRoute] fails to generate a route.
///
/// This callback is typically used for error handling. For example, this
/// callback might always generate a "not found" page that describes the route
/// that wasn't found.
///
/// Unknown routes can arise either from errors in the app or from external
/// requests to push routes, such as from Android intents.
final RouteFactory onUnknownRoute;
/// The name of the first route to show.
///
/// Defaults to [Window.defaultRouteName], which may be overridden by the code
/// that launched the application.
///
/// If the route contains slashes, then it is treated as a "deep link", and
/// before this route is pushed, the routes leading to this one are pushed
/// also. For example, if the route was `/a/b/c`, then the app would start
/// with the three routes `/a`, `/a/b`, and `/a/b/c` loaded, in that order.
///
/// If any part of this process fails to generate routes, then the
/// [initialRoute] is ignored and [Navigator.defaultRouteName] is used instead
/// (`/`). This can happen if the app is started with an intent that specifies
/// a non-existent route.
///
/// See also:
///
/// * [Navigator.initialRoute], which is used to implement this property.
/// * [Navigator.push], for pushing additional routes.
/// * [Navigator.pop], for removing a route from the stack.
final String initialRoute;
/// The initial locale for this app's [Localizations] widget.
///
/// If the 'locale' is null the system's locale value is used.
@ -298,12 +363,6 @@ class WidgetsApp extends StatefulWidget {
/// representative of what will happen in release mode.
final bool debugShowCheckedModeBanner;
/// The list of observers for the [Navigator] created for this app.
///
/// This list must be replaced by a list of newly-created observers if the
/// [navigatorKey] is changed.
final List<NavigatorObserver> navigatorObservers;
/// If true, forces the performance overlay to be visible in all instances.
///
/// Used by the `showPerformanceOverlay` observatory extension.
@ -332,7 +391,72 @@ class WidgetsApp extends StatefulWidget {
}
class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserver {
// STATE LIFECYCLE
@override
void initState() {
super.initState();
_updateNavigator();
_locale = _resolveLocale(ui.window.locale, widget.supportedLocales);
WidgetsBinding.instance.addObserver(this);
}
@override
void didUpdateWidget(WidgetsApp oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.navigatorKey != oldWidget.navigatorKey)
_updateNavigator();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) { }
@override
void didHaveMemoryPressure() { }
// NAVIGATOR
GlobalKey<NavigatorState> _navigator;
void _updateNavigator() {
if (widget.onGenerateRoute == null) {
_navigator = null;
} else {
_navigator = widget.navigatorKey ?? new GlobalObjectKey<NavigatorState>(this);
}
}
// On Android: the user has pressed the back button.
@override
Future<bool> didPopRoute() async {
assert(mounted);
final NavigatorState navigator = _navigator?.currentState;
if (navigator == null)
return false;
return await navigator.maybePop();
}
@override
Future<bool> didPushRoute(String route) async {
assert(mounted);
final NavigatorState navigator = _navigator?.currentState;
if (navigator == null)
return false;
navigator.pushNamed(route);
return true;
}
// LOCALIZATION
Locale _locale;
Locale _resolveLocale(Locale newLocale, Iterable<Locale> supportedLocales) {
@ -352,66 +476,6 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
return matchesLanguageCode ?? supportedLocales.first;
}
@override
void initState() {
super.initState();
_updateNavigator();
_locale = _resolveLocale(ui.window.locale, widget.supportedLocales);
WidgetsBinding.instance.addObserver(this);
}
@override
void didUpdateWidget(WidgetsApp oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.navigatorKey != oldWidget.navigatorKey)
_updateNavigator();
}
void _updateNavigator() {
_navigator = widget.navigatorKey ?? new GlobalObjectKey<NavigatorState>(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
// On Android: the user has pressed the back button.
@override
Future<bool> didPopRoute() async {
assert(mounted);
final NavigatorState navigator = _navigator.currentState;
assert(navigator != null);
return await navigator.maybePop();
}
@override
Future<bool> didPushRoute(String route) async {
assert(mounted);
final NavigatorState navigator = _navigator.currentState;
assert(navigator != null);
navigator.pushNamed(route);
return true;
}
@override
void didChangeMetrics() {
setState(() {
// The properties of ui.window have changed. We use them in our build
// function, so we need setState(), but we don't cache anything locally.
});
}
@override
void didChangeTextScaleFactor() {
setState(() {
// The textScaleFactor property of ui.window has changed. We reference
// ui.window in our build function, so we need to call setState(), but
// we don't need to cache anything locally.
});
}
@override
void didChangeLocale(Locale locale) {
if (locale == _locale)
@ -435,21 +499,53 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
yield DefaultWidgetsLocalizations.delegate;
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) { }
// METRICS
@override
void didHaveMemoryPressure() { }
void didChangeMetrics() {
setState(() {
// The properties of ui.window have changed. We use them in our build
// function, so we need setState(), but we don't cache anything locally.
});
}
@override
void didChangeTextScaleFactor() {
setState(() {
// The textScaleFactor property of ui.window has changed. We reference
// ui.window in our build function, so we need to call setState(), but
// we don't need to cache anything locally.
});
}
// BUILDER
@override
Widget build(BuildContext context) {
Widget result = new Navigator(
key: _navigator,
initialRoute: widget.initialRoute ?? ui.window.defaultRouteName,
onGenerateRoute: widget.onGenerateRoute,
onUnknownRoute: widget.onUnknownRoute,
observers: widget.navigatorObservers,
);
Widget navigator;
if (_navigator != null) {
navigator = new Navigator(
key: _navigator,
initialRoute: widget.initialRoute ?? ui.window.defaultRouteName,
onGenerateRoute: widget.onGenerateRoute,
onUnknownRoute: widget.onUnknownRoute,
observers: widget.navigatorObservers,
);
}
Widget result;
if (widget.builder != null) {
result = new Builder(
builder: (BuildContext context) {
return widget.builder(context, navigator);
},
);
} else {
assert(navigator != null);
result = navigator;
}
if (widget.textStyle != null) {
result = new DefaultTextStyle(
@ -502,28 +598,36 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
return true;
}());
Widget title;
if (widget.onGenerateTitle != null) {
title = new Builder(
// This Builder exists to provide a context below the Localizations widget.
// The onGenerateCallback() can refer to Localizations via its context
// parameter.
builder: (BuildContext context) {
final String title = widget.onGenerateTitle(context);
assert(title != null, 'onGenerateTitle must return a non-null String');
return new Title(
title: title,
color: widget.color,
child: result,
);
},
);
} else {
title = new Title(
title: widget.title,
color: widget.color,
child: result,
);
}
return new MediaQuery(
data: new MediaQueryData.fromWindow(ui.window),
child: new Localizations(
locale: widget.locale ?? _locale,
delegates: _localizationsDelegates.toList(),
// This Builder exists to provide a context below the Localizations widget.
// The onGenerateCallback() can refer to Localizations via its context
// parameter.
child: new Builder(
builder: (BuildContext context) {
String title = widget.title;
if (widget.onGenerateTitle != null) {
title = widget.onGenerateTitle(context);
assert(title != null, 'onGenerateTitle must return a non-null String');
}
return new Title(
title: title,
color: widget.color,
child: result,
);
},
),
child: title,
),
);
}

View File

@ -3557,6 +3557,14 @@ typedef Widget WidgetBuilder(BuildContext context);
/// Used by [ListView.builder] and other APIs that use lazily-generated widgets.
typedef Widget IndexedWidgetBuilder(BuildContext context, int index);
/// A builder that builds a widget given a child.
///
/// The child should typically be part of the returned widget tree.
///
/// Used by [AnimatedBuilder.builder], as well as [WidgetsApp.builder] and
/// [MaterialApp.builder].
typedef Widget TransitionBuilder(BuildContext context, Widget child);
/// An [Element] that composes other [Element]s.
///
/// Rather than creating a [RenderObject] directly, a [ComponentElement] creates

View File

@ -527,13 +527,6 @@ class AlignTransition extends AnimatedWidget {
}
}
/// A builder that builds a widget given a child.
///
/// The child should typically be part of the returned widget tree.
///
/// Used by [AnimatedBuilder.builder].
typedef Widget TransitionBuilder(BuildContext context, Widget child);
/// A general-purpose widget for building animations.
///
/// AnimatedBuilder is useful for more complex widgets that wish to include

View File

@ -0,0 +1,65 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
void main() {
testWidgets('builder doesn\'t get called if app doesn\'t change', (WidgetTester tester) async {
final List<String> log = <String>[];
final Widget app = new MaterialApp(
theme: new ThemeData(
primarySwatch: Colors.green,
),
home: const Placeholder(),
builder: (BuildContext context, Widget child) {
log.add('build');
expect(Theme.of(context).primaryColor, Colors.green.shade500);
expect(Directionality.of(context), TextDirection.ltr);
expect(child, const isInstanceOf<Navigator>());
return const Placeholder();
},
);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.rtl,
child: app,
),
);
expect(log, <String>['build']);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: app,
),
);
expect(log, <String>['build']);
});
testWidgets('builder doesn\'t get called if app doesn\'t change', (WidgetTester tester) async {
final List<String> log = <String>[];
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(
primarySwatch: Colors.yellow,
),
home: new Builder(
builder: (BuildContext context) {
log.add('build');
expect(Theme.of(context).primaryColor, Colors.yellow.shade500);
expect(Directionality.of(context), TextDirection.rtl);
return const Placeholder();
},
),
builder: (BuildContext context, Widget child) {
return new Directionality(
textDirection: TextDirection.rtl,
child: child,
);
},
),
);
expect(log, <String>['build']);
});
}