Unverified Commit c12d120b authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Provide a way to override global InheritedWidgets (#14348)

For example, so that the gallery can override the media query globally.
parent 12ceaefb
......@@ -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),
);
},
);
}
}
......@@ -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(),
......
......@@ -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(
......
......@@ -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.
......
......@@ -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.
......
......@@ -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,37 +164,11 @@ 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.
final GlobalKey<NavigatorState> navigatorKey;
/// 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
/// displayed when the user presses the "recent apps" button. Similarly, on
/// iOS the titles appear in the App Switcher when the user double presses the
/// home button.
///
/// To provide a localized title instead, use [onGenerateTitle].
///
/// This value is passed unmodified to [WidgetsApp.title].
final String title;
/// If non-null this callback is called to produce the app's
/// title string, otherwise [title] is used.
///
/// The [onGenerateTitle] `context` parameter includes the [WidgetsApp]'s
/// [Localizations] widget so that this callback can be used to produce a
/// localized title.
///
/// This callback function must not return null.
///
/// The [onGenerateTitle] callback is called each time the [MaterialApp]
/// rebuilds.
///
/// This value is passed unmodified to [WidgetsApp.onGenerateTitle].
final GenerateAppTitle onGenerateTitle;
/// The colors to use for the application's widgets.
final ThemeData theme;
/// 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 `/`).
......@@ -185,14 +183,19 @@ class MaterialApp extends StatefulWidget {
///
/// 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.
///
/// For example, on Android this is the color used for the application in the
/// application switcher.
final Color color;
/// 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.
///
......@@ -210,9 +213,13 @@ class MaterialApp extends StatefulWidget {
/// 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.
/// 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.
......@@ -227,6 +234,10 @@ class MaterialApp extends StatefulWidget {
/// (`/`). 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.
......@@ -246,6 +257,10 @@ class MaterialApp extends StatefulWidget {
/// 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
......@@ -257,8 +272,102 @@ class MaterialApp extends StatefulWidget {
///
/// 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
/// displayed when the user presses the "recent apps" button. Similarly, on
/// iOS the titles appear in the App Switcher when the user double presses the
/// home button.
///
/// To provide a localized title instead, use [onGenerateTitle].
///
/// This value is passed unmodified to [WidgetsApp.title].
final String title;
/// If non-null this callback is called to produce the app's
/// title string, otherwise [title] is used.
///
/// The [onGenerateTitle] `context` parameter includes the [WidgetsApp]'s
/// [Localizations] widget so that this callback can be used to produce a
/// localized title.
///
/// This callback function must not return null.
///
/// The [onGenerateTitle] callback is called each time the [MaterialApp]
/// rebuilds.
///
/// This value is passed unmodified to [WidgetsApp.onGenerateTitle].
final GenerateAppTitle onGenerateTitle;
/// The colors to use for the application's widgets.
final ThemeData theme;
/// The primary color to use for the application in the operating system
/// interface.
///
/// For example, on Android this is the color used for the application in the
/// application switcher.
final Color color;
/// 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,
......
......@@ -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,40 +119,10 @@ 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.
final GlobalKey<NavigatorState> navigatorKey;
/// 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
/// displayed when the user presses the "recent apps" button. Similarly, on
/// iOS the titles appear in the App Switcher when the user double presses the
/// home button.
///
/// To provide a localized title instead, use [onGenerateTitle].
final String title;
/// If non-null this callback function is called to produce the app's
/// title string, otherwise [title] is used.
///
/// The [onGenerateTitle] `context` parameter includes the [WidgetsApp]'s
/// [Localizations] widget so that this callback can be used to produce a
/// localized title.
///
/// This callback function must not return null.
///
/// The [onGenerateTitle] callback is called each time the [WidgetsApp]
/// rebuilds.
final GenerateAppTitle onGenerateTitle;
/// The default text style for [Text] in the application.
final TextStyle textStyle;
/// The primary color to use for the application in the operating system
/// interface.
///
/// For example, on Android this is the color used for the application in the
/// application switcher.
final Color color;
/// 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.
......@@ -156,6 +134,9 @@ class WidgetsApp extends StatefulWidget {
/// 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.
......@@ -166,6 +147,9 @@ class WidgetsApp extends StatefulWidget {
///
/// 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.
......@@ -183,6 +167,9 @@ class WidgetsApp extends StatefulWidget {
/// (`/`). 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.
......@@ -190,6 +177,84 @@ class WidgetsApp extends StatefulWidget {
/// * [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
/// displayed when the user presses the "recent apps" button. Similarly, on
/// iOS the titles appear in the App Switcher when the user double presses the
/// home button.
///
/// To provide a localized title instead, use [onGenerateTitle].
final String title;
/// If non-null this callback function is called to produce the app's
/// title string, otherwise [title] is used.
///
/// The [onGenerateTitle] `context` parameter includes the [WidgetsApp]'s
/// [Localizations] widget so that this callback can be used to produce a
/// localized title.
///
/// This callback function must not return null.
///
/// The [onGenerateTitle] callback is called each time the [WidgetsApp]
/// rebuilds.
final GenerateAppTitle onGenerateTitle;
/// The default text style for [Text] in the application.
final TextStyle textStyle;
/// The primary color to use for the application in the operating system
/// interface.
///
/// For example, on Android this is the color used for the application in the
/// application switcher.
final Color color;
/// 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,25 +391,8 @@ class WidgetsApp extends StatefulWidget {
}
class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserver {
GlobalKey<NavigatorState> _navigator;
Locale _locale;
Locale _resolveLocale(Locale newLocale, Iterable<Locale> supportedLocales) {
if (widget.localeResolutionCallback != null) {
final Locale locale = widget.localeResolutionCallback(newLocale, widget.supportedLocales);
if (locale != null)
return locale;
}
Locale matchesLanguageCode;
for (Locale locale in supportedLocales) {
if (locale == newLocale)
return newLocale;
if (locale.languageCode == newLocale.languageCode)
matchesLanguageCode ??= locale;
}
return matchesLanguageCode ?? supportedLocales.first;
}
// STATE LIFECYCLE
@override
void initState() {
......@@ -367,49 +409,71 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
_updateNavigator();
}
void _updateNavigator() {
_navigator = widget.navigatorKey ?? new GlobalObjectKey<NavigatorState>(this);
}
@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;
assert(navigator != null);
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;
assert(navigator != null);
final NavigatorState navigator = _navigator?.currentState;
if (navigator == null)
return false;
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.
});
// LOCALIZATION
Locale _locale;
Locale _resolveLocale(Locale newLocale, Iterable<Locale> supportedLocales) {
if (widget.localeResolutionCallback != null) {
final Locale locale = widget.localeResolutionCallback(newLocale, widget.supportedLocales);
if (locale != null)
return locale;
}
@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.
});
Locale matchesLanguageCode;
for (Locale locale in supportedLocales) {
if (locale == newLocale)
return newLocale;
if (locale.languageCode == newLocale.languageCode)
matchesLanguageCode ??= locale;
}
return matchesLanguageCode ?? supportedLocales.first;
}
@override
......@@ -435,21 +499,53 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
yield DefaultWidgetsLocalizations.delegate;
}
// METRICS
@override
void didChangeAppLifecycleState(AppLifecycleState state) { }
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 didHaveMemoryPressure() { }
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(
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;
}());
return new MediaQuery(
data: new MediaQueryData.fromWindow(ui.window),
child: new Localizations(
locale: widget.locale ?? _locale,
delegates: _localizationsDelegates.toList(),
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.
child: new Builder(
builder: (BuildContext context) {
String title = widget.title;
if (widget.onGenerateTitle != null) {
title = widget.onGenerateTitle(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(),
child: title,
),
);
}
......
......@@ -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
......
......@@ -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
......
// 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']);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment