diff --git a/examples/flutter_gallery/lib/gallery/app.dart b/examples/flutter_gallery/lib/gallery/app.dart index dd318c431d..c34c54dd80 100644 --- a/examples/flutter_gallery/lib/gallery/app.dart +++ b/examples/flutter_gallery/lib/gallery/app.dart @@ -51,6 +51,7 @@ class GalleryAppState extends State { 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 { _textScaleFactor = value; }); }, + overrideDirection: _overrideDirection, + onOverrideDirectionChanged: (TextDirection value) { + setState(() { + _overrideDirection = value; + }); + }, onSendFeedback: widget.onSendFeedback, ); @@ -155,7 +162,7 @@ class GalleryAppState extends State { // 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 { checkerboardOffscreenLayers: _checkerboardOffscreenLayers, routes: _kRoutes, home: _applyScaleFactor(home), + builder: (BuildContext context, Widget child) { + return new Directionality( + textDirection: _overrideDirection, + child: _applyScaleFactor(child), + ); + }, ); } } diff --git a/examples/flutter_gallery/lib/gallery/drawer.dart b/examples/flutter_gallery/lib/gallery/drawer.dart index 5a4c0a2ef3..fabe75acf9 100644 --- a/examples/flutter_gallery/lib/gallery/drawer.dart +++ b/examples/flutter_gallery/lib/gallery/drawer.dart @@ -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 onPlatformChanged; + final TextDirection overrideDirection; + final ValueChanged 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([ + overrideDirectionItem, const Divider(), animateSlowlyItem, const Divider(), diff --git a/examples/flutter_gallery/lib/gallery/home.dart b/examples/flutter_gallery/lib/gallery/home.dart index d44a9d8f63..8131adbbdf 100644 --- a/examples/flutter_gallery/lib/gallery/home.dart +++ b/examples/flutter_gallery/lib/gallery/home.dart @@ -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 onPlatformChanged; + final TextDirection overrideDirection; + final ValueChanged onOverrideDirectionChanged; + final VoidCallback onSendFeedback; @override @@ -177,6 +182,8 @@ class GalleryHomeState extends State with SingleTickerProviderState checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, onCheckerboardOffscreenLayersChanged: widget.onCheckerboardOffscreenLayersChanged, onPlatformChanged: widget.onPlatformChanged, + overrideDirection: widget.overrideDirection, + onOverrideDirectionChanged: widget.onOverrideDirectionChanged, onSendFeedback: widget.onSendFeedback, ), body: new CustomScrollView( diff --git a/examples/flutter_gallery/test/drawer_test.dart b/examples/flutter_gallery/test/drawer_test.dart index ce6b24b49e..5a108b34ae 100644 --- a/examples/flutter_gallery/test/drawer_test.dart +++ b/examples/flutter_gallery/test/drawer_test.dart @@ -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. diff --git a/examples/flutter_gallery/test/smoke_test.dart b/examples/flutter_gallery/test/smoke_test.dart index e06afbb28a..7b8d9a37b4 100644 --- a/examples/flutter_gallery/test/smoke_test.dart +++ b/examples/flutter_gallery/test/smoke_test.dart @@ -159,7 +159,7 @@ Future 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. diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index 8fac0a4007..fa88eeaa67 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -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 {}, this.initialRoute, this.onGenerateRoute, this.onUnknownRoute, + this.navigatorObservers: const [], + this.builder, + this.title: '', + this.onGenerateTitle, + this.color, + this.theme, this.locale, this.localizationsDelegates, this.localeResolutionCallback, this.supportedLocales: const [const Locale('en', 'US')], - this.navigatorObservers: const [], 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 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 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 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 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 navigatorObservers; - /// Turns on a [GridPaper] overlay that paints a baseline grid /// Material apps. /// @@ -469,6 +572,7 @@ class _MaterialAppState extends State { void initState() { super.initState(); _heroController = new HeroController(createRectTween: _createRectTween); + _updateNavigator(); } @override @@ -481,17 +585,19 @@ class _MaterialAppState extends State { // 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> get _localizationsDelegates sync* { - if (widget.localizationsDelegates != null) - yield* widget.localizationsDelegates; - yield DefaultMaterialLocalizations.delegate; + bool _haveNavigator; + List _navigatorObservers; + + void _updateNavigator() { + _haveNavigator = widget.home != null || + widget.routes.isNotEmpty || + widget.onGenerateRoute != null || + widget.onUnknownRoute != null; + _navigatorObservers = new List.from(widget.navigatorObservers) + ..add(_heroController); } RectTween _createRectTween(Rect begin, Rect end) { @@ -548,6 +654,17 @@ class _MaterialAppState extends State { 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> 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 { 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.from(widget.navigatorObservers) - ..add(_heroController), - initialRoute: widget.initialRoute, - onGenerateRoute: _onGenerateRoute, - onUnknownRoute: _onUnknownRoute, locale: widget.locale, localizationsDelegates: _localizationsDelegates, localeResolutionCallback: widget.localeResolutionCallback, diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 4b4ab884de..7c06728056 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -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 [], + this.initialRoute, + this.builder, this.title: '', this.onGenerateTitle, this.textStyle, @required this.color, - this.navigatorObservers: const [], - 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 []), + 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 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 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 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 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 _navigator; + + void _updateNavigator() { + if (widget.onGenerateRoute == null) { + _navigator = null; + } else { + _navigator = widget.navigatorKey ?? new GlobalObjectKey(this); + } + } + + // On Android: the user has pressed the back button. + @override + Future didPopRoute() async { + assert(mounted); + final NavigatorState navigator = _navigator?.currentState; + if (navigator == null) + return false; + return await navigator.maybePop(); + } + + @override + Future 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 supportedLocales) { @@ -352,66 +476,6 @@ class _WidgetsAppState extends State 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(this); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - // On Android: the user has pressed the back button. - @override - Future didPopRoute() async { - assert(mounted); - final NavigatorState navigator = _navigator.currentState; - assert(navigator != null); - return await navigator.maybePop(); - } - - @override - Future 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 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 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, ), ); } diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 5ef2fb54c6..1a4996be94 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -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 diff --git a/packages/flutter/lib/src/widgets/transitions.dart b/packages/flutter/lib/src/widgets/transitions.dart index 8acfe5c54f..9d2d4099ae 100644 --- a/packages/flutter/lib/src/widgets/transitions.dart +++ b/packages/flutter/lib/src/widgets/transitions.dart @@ -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 diff --git a/packages/flutter/test/material/app_builder_test.dart b/packages/flutter/test/material/app_builder_test.dart new file mode 100644 index 0000000000..e4cb5e3c1d --- /dev/null +++ b/packages/flutter/test/material/app_builder_test.dart @@ -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 log = []; + 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()); + return const Placeholder(); + }, + ); + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.rtl, + child: app, + ), + ); + expect(log, ['build']); + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: app, + ), + ); + expect(log, ['build']); + }); + + testWidgets('builder doesn\'t get called if app doesn\'t change', (WidgetTester tester) async { + final List log = []; + 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, ['build']); + }); +}