// 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 'dart:async'; import 'package:flutter/rendering.dart'; import 'package:flutter/foundation.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'; const TextStyle _errorTextStyle = const TextStyle( color: const Color(0xD0FF0000), fontFamily: 'monospace', fontSize: 48.0, fontWeight: FontWeight.w900, decoration: TextDecoration.underline, decorationColor: const Color(0xFFFFFF00), decorationStyle: TextDecorationStyle.double ); class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> { const _MaterialLocalizationsDelegate(); @override Future<MaterialLocalizations> load(Locale locale) => DefaultMaterialLocalizations.load(locale); @override bool shouldReload(_MaterialLocalizationsDelegate old) => false; } /// 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. /// /// 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. /// /// 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], or [onGenerateRoute] must be given. 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.title: '', this.onGenerateTitle, this.color, this.theme, this.home, this.routes: const <String, WidgetBuilder>{}, this.initialRoute, this.onGenerateRoute, this.onUnknownRoute, 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), 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), 'If the home property is specified, the routes table ' 'cannot include an entry for "/", since it would be redundant.' ), assert( 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, ' 'because otherwise there is nothing to fall back on if the ' 'app is started with an intent that specifies an unknown route.' ), super(key: key); /// 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 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. /// /// This value is passed unmodified to [WidgetsApp.onGenerateTitle]. final GenerateAppTitle onGenerateTitle; /// 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. /// /// For example, on Android this is the color used for the application in the /// application switcher. final Color color; /// The application's top-level routing table. /// /// When a named route is pushed with [Navigator.pushNamed], the route name is /// looked up in this map. If the name is present, the associated /// [WidgetBuilder] is used to construct a [MaterialPageRoute] that performs /// an appropriate transition, including [Hero] animations, to the new route. /// /// If the app only has one page, then you can specify it using [home] instead. /// /// If [home] is specified, then it implies an entry in this table for the /// [Navigator.defaultRouteName] route (`/`), and it is an error to /// redundantly provide such a route in the [routes] table. /// /// If a route is requested that is not specified in this table (or by /// [home]), then the [onGenerateRoute] callback is called to build the page /// instead. final Map<String, WidgetBuilder> routes; /// The name of the first route to show. /// /// Defaults to [Window.defaultRouteName], which may be overridden by the code /// that launched the application. /// /// If the route contains slashes, then it is treated as a "deep link", and /// before this route is pushed, the routes leading to this one are pushed /// also. For example, if the route was `/a/b/c`, then the app would start /// with the three routes `/a`, `/a/b`, and `/a/b/c` loaded, in that order. /// /// If any part of this process fails to generate routes, then the /// [initialRoute] is ignored and [Navigator.defaultRouteName] is used instead /// (`/`). This can happen if the app is started with an intent that specifies /// a non-existent route. /// /// See also: /// /// * [Navigator.initialRoute], which is used to implement this property. /// * [Navigator.push], for pushing additional routes. /// * [Navigator.pop], for removing a route from the stack. final String initialRoute; /// The route generator callback used when the app is navigated to a /// named route. /// /// This is used if [routes] does not contain the requested route. /// /// If this returns null when building the routes to handle the specified /// [initialRoute], then all the routes are discarded and /// [Navigator.defaultRouteName] is used instead (`/`). See [initialRoute]. /// /// During normal app operation, the [onGenerateRoute] callback will only be /// applied to route names pushed by the application, and so should never /// return null. final RouteFactory onGenerateRoute; /// Called when [onGenerateRoute] fails to generate a route, except for the /// [initialRoute]. /// /// This callback is typically used for error handling. For example, this /// callback might always generate a "not found" page that describes the route /// that wasn't found. /// /// The default implementation pushes a route that displays an ugly error /// message. final RouteFactory onUnknownRoute; /// The initial locale for this app's [Localizations] widget. /// /// If the `locale` is null the system's locale value is used. final Locale locale; /// The delegates for this app's [Localizations] widget. /// /// The delegates collectively define all of the localized resources /// for this application's [Localizations] widget. /// /// 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; /// This callback is responsible for choosing the app's locale /// when the app is started, and when the user changes the /// device's locale. /// /// The returned value becomes the locale of this app's [Localizations] /// widget. The callback's `locale` parameter is the device's locale when /// the app started, or the device locale the user selected after the app was /// started. The callback's `supportedLocales` parameter is just the value /// [supportedLocales]. /// /// An app could use this callback to substitute locales based on the app's /// intended audience. If the device's OS provides a prioritized /// list of locales, this callback could be used to defer to it. /// /// If the callback is null then the resolved locale is: /// - The callback's `locale` parameter if it's equal to a supported locale. /// - The first supported locale with the same [Locale.languageCode] as the /// callback's `locale` parameter. /// - The first supported locale. /// /// This callback is passed along to the [WidgetsApp] built by this widget. final LocaleResolutionCallback localeResolutionCallback; /// The list of locales that this app has been localized for. /// /// By default only the American English locale is supported. Apps should /// configure this list to match the locales they support. /// /// This list must not null. It's default value is just /// `[const Locale('en', 'US')]`. It is passed along unmodified to the /// [WidgetsApp] built by this widget. /// /// The order of the list matters. By default, if the device's locale doesn't /// exactly match a locale in [supportedLocales] then the first locale in /// [supportedLocales] with a matching [Locale.languageCode] is used. If that /// fails then the first locale in [supportedLocales] is used. The default /// locale resolution algorithm can be overridden with [localeResolutionCallback]. /// /// 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 /// 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; /// Turns on a little "SLOW MODE" banner in checked mode to indicate /// that the app is in checked mode. This is on by default (in /// checked mode), to turn it off, set the constructor argument to /// false. In release mode this has no effect. /// /// To get this banner in your application if you're not using /// WidgetsApp, include a [CheckedModeBanner] widget in your app. /// /// This banner is intended to deter people from complaining that your /// app is slow when it's in checked mode. In checked mode, Flutter /// enables a large number of expensive diagnostics to aid in /// development, and so performance in checked mode is not /// representative of what will happen in release mode. final bool debugShowCheckedModeBanner; /// The list of observers for the [Navigator] created for this app. final List<NavigatorObserver> navigatorObservers; /// 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); } // 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 const _MaterialLocalizationsDelegate(); } 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; } @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), 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, 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, ); } }