| // Copyright 2015 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/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'arc.dart'; |
| import 'colors.dart'; |
| import 'floating_action_button.dart'; |
| import 'icons.dart'; |
| import 'material_localizations.dart'; |
| import 'page.dart'; |
| import 'theme.dart'; |
| |
| /// [MaterialApp] uses this [TextStyle] as its [DefaultTextStyle] to encourage |
| /// developers to be intentional about their [DefaultTextStyle]. |
| /// |
| /// In Material Design, most [Text] widgets are contained in [Material] widgets, |
| /// which sets a specific [DefaultTextStyle]. If you're seeing text that uses |
| /// this text style, consider putting your text in a [Material] widget (or |
| /// another widget that sets a [DefaultTextStyle]). |
| const TextStyle _errorTextStyle = TextStyle( |
| color: Color(0xD0FF0000), |
| fontFamily: 'monospace', |
| fontSize: 48.0, |
| fontWeight: FontWeight.w900, |
| decoration: TextDecoration.underline, |
| decorationColor: Color(0xFFFFFF00), |
| decorationStyle: TextDecorationStyle.double, |
| debugLabel: 'fallback style; consider putting your text in a Material', |
| ); |
| |
| /// An application that uses material design. |
| /// |
| /// A convenience widget that wraps a number of widgets that are commonly |
| /// required for material design applications. It builds upon a [WidgetsApp] by |
| /// adding material-design specific functionality, such as [AnimatedTheme] and |
| /// [GridPaper]. |
| /// |
| /// The [MaterialApp] configures the top-level [Navigator] to search for routes |
| /// in the following order: |
| /// |
| /// 1. For the `/` route, the [home] property, if non-null, is used. |
| /// |
| /// 2. Otherwise, the [routes] table is used, if it has an entry for the route. |
| /// |
| /// 3. Otherwise, [onGenerateRoute] is called, if provided. It should return a |
| /// non-null value for any _valid_ route not handled by [home] and [routes]. |
| /// |
| /// 4. Finally if all else fails [onUnknownRoute] is called. |
| /// |
| /// 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 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: |
| /// |
| /// * [Scaffold], which provides standard app elements like an [AppBar] and a [Drawer]. |
| /// * [Navigator], which is used to manage the app's stack of pages. |
| /// * [MaterialPageRoute], which defines an app page that transitions in a material-specific way. |
| /// * [WidgetsApp], which defines the basic app elements but does not depend on the material library. |
| class MaterialApp extends StatefulWidget { |
| /// Creates a MaterialApp. |
| /// |
| /// 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. |
| /// |
| /// This class creates an instance of [WidgetsApp]. |
| /// |
| /// The boolean arguments, [routes], and [navigatorObservers], must not be null. |
| MaterialApp({ // can't be const because the asserts use methods on Map :-( |
| Key key, |
| this.navigatorKey, |
| 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>[Locale('en', 'US')], |
| this.debugShowMaterialGrid = false, |
| this.showPerformanceOverlay = false, |
| this.checkerboardRasterCacheImages = false, |
| this.checkerboardOffscreenLayers = false, |
| this.showSemanticsDebugger = false, |
| this.debugShowCheckedModeBanner = true, |
| }) : assert(routes != null), |
| assert(navigatorObservers != null), |
| assert( |
| home == null || |
| !routes.containsKey(Navigator.defaultRouteName), |
| 'If the home property is specified, the routes table ' |
| 'cannot include an entry for "/", since it would be redundant.' |
| ), |
| assert( |
| builder != null || |
| home != null || |
| routes.containsKey(Navigator.defaultRouteName) || |
| onGenerateRoute != null || |
| onUnknownRoute != null, |
| 'Either the home property must be specified, ' |
| '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]. |
| /// |
| /// If a [navigatorKey] is specified, the [Navigator] can be directly |
| /// manipulated without first obtaining it from a [BuildContext] via |
| /// [Navigator.of]: from the [navigatorKey], use the [GlobalKey.currentState] |
| /// getter. |
| /// |
| /// If this is changed, a new [Navigator] will be created, losing all the |
| /// 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; |
| |
| /// {@macro flutter.widgets.widgetsApp.initialRoute} |
| /// |
| /// 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; |
| |
| /// {@macro flutter.widgets.widgetsApp.onGenerateRoute} |
| /// |
| /// This is used if [routes] does not contain the requested route. |
| /// |
| /// 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]. |
| /// |
| /// {@macro flutter.widgets.widgetsApp.onUnknownRoute} |
| /// |
| /// 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; |
| |
| /// {@macro flutter.widgets.widgetsApp.navigatorObservers} |
| /// |
| /// 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; |
| |
| /// {@macro flutter.widgets.widgetsApp.builder} |
| /// |
| /// 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. |
| final TransitionBuilder builder; |
| |
| /// {@macro flutter.widgets.widgetsApp.title} |
| /// |
| /// This value is passed unmodified to [WidgetsApp.title]. |
| final String title; |
| |
| /// {@macro flutter.widgets.widgetsApp.onGenerateTitle} |
| /// |
| /// This value is passed unmodified to [WidgetsApp.onGenerateTitle]. |
| final GenerateAppTitle onGenerateTitle; |
| |
| /// The colors to use for the application's widgets. |
| final ThemeData theme; |
| |
| /// {@macro flutter.widgets.widgetsApp.color} |
| final Color color; |
| |
| /// {@macro flutter.widgets.widgetsApp.locale} |
| final Locale locale; |
| |
| /// {@macro flutter.widgets.widgetsApp.localizationsDelegates} |
| /// |
| /// Delegates that produce [WidgetsLocalizations] and [MaterialLocalizations] |
| /// are included automatically. Apps can provide their own versions of these |
| /// localizations by creating implementations of |
| /// [LocalizationsDelegate<WidgetsLocalizations>] or |
| /// [LocalizationsDelegate<MaterialLocalizations>] whose load methods return |
| /// custom versions of [WidgetsLocalizations] or [MaterialLocalizations]. |
| /// |
| /// For example: to add support to [MaterialLocalizations] for a |
| /// locale it doesn't already support, say `const Locale('foo', 'BR')`, |
| /// one could just extend [DefaultMaterialLocalizations]: |
| /// |
| /// ```dart |
| /// class FooLocalizations extends DefaultMaterialLocalizations { |
| /// FooLocalizations(Locale locale) : super(locale); |
| /// @override |
| /// String get okButtonLabel { |
| /// if (locale == const Locale('foo', 'BR')) |
| /// return 'foo'; |
| /// return super.okButtonLabel; |
| /// } |
| /// } |
| /// |
| /// ``` |
| /// |
| /// A `FooLocalizationsDelegate` is essentially just a method that constructs |
| /// a `FooLocalizations` object. We return a [SynchronousFuture] here because |
| /// no asynchronous work takes place upon "loading" the localizations object. |
| /// |
| /// ```dart |
| /// class FooLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> { |
| /// const FooLocalizationsDelegate(); |
| /// @override |
| /// Future<FooLocalizations> load(Locale locale) { |
| /// return new SynchronousFuture(new FooLocalizations(locale)); |
| /// } |
| /// @override |
| /// bool shouldReload(FooLocalizationsDelegate old) => false; |
| /// } |
| /// ``` |
| /// |
| /// Constructing a [MaterialApp] with a `FooLocalizationsDelegate` overrides |
| /// the automatically included delegate for [MaterialLocalizations] because |
| /// only the first delegate of each [LocalizationsDelegate.type] is used and |
| /// the automatically included delegates are added to the end of the app's |
| /// [localizationsDelegates] list. |
| /// |
| /// ```dart |
| /// new MaterialApp( |
| /// localizationsDelegates: [ |
| /// const FooLocalizationsDelegate(), |
| /// ], |
| /// // ... |
| /// ) |
| /// ``` |
| final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates; |
| |
| /// {@macro flutter.widgets.widgetsApp.localeResolutionCallback} |
| /// |
| /// This callback is passed along to the [WidgetsApp] built by this widget. |
| final LocaleResolutionCallback localeResolutionCallback; |
| |
| /// {@macro flutter.widgets.widgetsApp.supportedLocales} |
| /// |
| /// It is passed along unmodified to the [WidgetsApp] built by this widget. |
| /// |
| /// The material widgets include translations for locales with the following |
| /// language codes: |
| /// ``` |
| /// ar - Arabic |
| /// de - German |
| /// en - English |
| /// es - Spanish |
| /// fa - Farsi (Persian) |
| /// fr - French |
| /// he - Hebrew |
| /// it - Italian |
| /// ja - Japanese |
| /// ps - Pashto |
| /// pt - Portugese |
| /// ro - Romanian |
| /// ru - Russian |
| /// sd - Sindhi |
| /// ur - Urdu |
| /// zh - Chinese (simplified) |
| /// ``` |
| final Iterable<Locale> supportedLocales; |
| |
| /// Turns on a performance overlay. |
| /// |
| /// See also: |
| /// |
| /// * <https://flutter.io/debugging/#performanceoverlay> |
| final bool showPerformanceOverlay; |
| |
| /// Turns on checkerboarding of raster cache images. |
| final bool checkerboardRasterCacheImages; |
| |
| /// Turns on checkerboarding of layers rendered to offscreen bitmaps. |
| final bool checkerboardOffscreenLayers; |
| |
| /// Turns on an overlay that shows the accessibility information |
| /// reported by the framework. |
| final bool showSemanticsDebugger; |
| |
| /// {@macro flutter.widgets.widgetsApp.debugShowCheckedModeBanner} |
| final bool debugShowCheckedModeBanner; |
| |
| /// Turns on a [GridPaper] overlay that paints a baseline grid |
| /// Material apps. |
| /// |
| /// Only available in checked mode. |
| /// |
| /// See also: |
| /// |
| /// * <https://material.google.com/layout/metrics-keylines.html> |
| final bool debugShowMaterialGrid; |
| |
| @override |
| _MaterialAppState createState() => new _MaterialAppState(); |
| } |
| |
| class _MaterialScrollBehavior extends ScrollBehavior { |
| @override |
| TargetPlatform getPlatform(BuildContext context) { |
| return Theme.of(context).platform; |
| } |
| |
| @override |
| Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { |
| // When modifying this function, consider modifying the implementation in |
| // the base class as well. |
| switch (getPlatform(context)) { |
| case TargetPlatform.iOS: |
| return child; |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| return new GlowingOverscrollIndicator( |
| child: child, |
| axisDirection: axisDirection, |
| color: Theme.of(context).accentColor, |
| ); |
| } |
| return null; |
| } |
| } |
| |
| class _MaterialAppState extends State<MaterialApp> { |
| HeroController _heroController; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _heroController = new HeroController(createRectTween: _createRectTween); |
| _updateNavigator(); |
| } |
| |
| @override |
| void didUpdateWidget(MaterialApp oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (widget.navigatorKey != oldWidget.navigatorKey) { |
| // If the Navigator changes, we have to create a new observer, because the |
| // old Navigator won't be disposed (and thus won't unregister with its |
| // observers) until after the new one has been created (because the |
| // Navigator has a GlobalKey). |
| _heroController = new HeroController(createRectTween: _createRectTween); |
| } |
| _updateNavigator(); |
| } |
| |
| 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) { |
| return new MaterialRectArcTween(begin: begin, end: end); |
| } |
| |
| Route<dynamic> _onGenerateRoute(RouteSettings settings) { |
| final String name = settings.name; |
| WidgetBuilder builder; |
| if (name == Navigator.defaultRouteName && widget.home != null) { |
| builder = (BuildContext context) => widget.home; |
| } else { |
| builder = widget.routes[name]; |
| } |
| if (builder != null) { |
| return new MaterialPageRoute<dynamic>( |
| builder: builder, |
| settings: settings, |
| ); |
| } |
| if (widget.onGenerateRoute != null) |
| return widget.onGenerateRoute(settings); |
| return null; |
| } |
| |
| Route<dynamic> _onUnknownRoute(RouteSettings settings) { |
| assert(() { |
| if (widget.onUnknownRoute == null) { |
| throw new FlutterError( |
| 'Could not find a generator for route $settings in the $runtimeType.\n' |
| 'Generators for routes are searched for in the following order:\n' |
| ' 1. For the "/" route, the "home" property, if non-null, is used.\n' |
| ' 2. Otherwise, the "routes" table is used, if it has an entry for ' |
| 'the route.\n' |
| ' 3. Otherwise, onGenerateRoute is called. It should return a ' |
| 'non-null value for any valid route not handled by "home" and "routes".\n' |
| ' 4. Finally if all else fails onUnknownRoute is called.\n' |
| 'Unfortunately, onUnknownRoute was not set.' |
| ); |
| } |
| return true; |
| }()); |
| final Route<dynamic> result = widget.onUnknownRoute(settings); |
| assert(() { |
| if (result == null) { |
| throw new FlutterError( |
| 'The onUnknownRoute callback returned null.\n' |
| 'When the $runtimeType requested the route $settings from its ' |
| 'onUnknownRoute callback, the callback returned null. Such callbacks ' |
| 'must never return null.' |
| ); |
| } |
| return true; |
| }()); |
| 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(); |
| Widget result = new AnimatedTheme( |
| data: theme, |
| isMaterialAppTheme: true, |
| 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, |
| locale: widget.locale, |
| localizationsDelegates: _localizationsDelegates, |
| localeResolutionCallback: widget.localeResolutionCallback, |
| supportedLocales: widget.supportedLocales, |
| showPerformanceOverlay: widget.showPerformanceOverlay, |
| checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, |
| checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, |
| showSemanticsDebugger: widget.showSemanticsDebugger, |
| debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner, |
| inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) { |
| return new FloatingActionButton( |
| child: const Icon(Icons.search), |
| onPressed: onPressed, |
| mini: true, |
| ); |
| }, |
| ) |
| ); |
| |
| assert(() { |
| if (widget.debugShowMaterialGrid) { |
| result = new GridPaper( |
| color: const Color(0xE0F9BBE0), |
| interval: 8.0, |
| divisions: 2, |
| subdivisions: 1, |
| child: result, |
| ); |
| } |
| return true; |
| }()); |
| |
| return new ScrollConfiguration( |
| behavior: new _MaterialScrollBehavior(), |
| child: result, |
| ); |
| } |
| } |