blob: b2eb132fdfe391f2f94cd52ced27d290ac78a3de [file] [log] [blame]
// 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.routes = const <String, WidgetBuilder>{},
this.navigatorObservers = const <NavigatorObserver>[],
this.title = '',
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),
home == null ||
'If the home property is specified, the routes table '
'cannot include an entry for "/", since it would be redundant.'
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.'
(home != null ||
routes.isNotEmpty ||
onGenerateRoute != null ||
onUnknownRoute != null)
(builder != null &&
navigatorKey == null &&
initialRoute == null &&
'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:
/// * <>
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:
/// * <>
final bool debugShowMaterialGrid;
_MaterialAppState createState() => new _MaterialAppState();
class _MaterialScrollBehavior extends ScrollBehavior {
TargetPlatform getPlatform(BuildContext context) {
return Theme.of(context).platform;
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.fuchsia:
return new GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
color: Theme.of(context).accentColor,
return null;
class _MaterialAppState extends State<MaterialApp> {
HeroController _heroController;
void initState() {
_heroController = new HeroController(createRectTween: _createRectTween);
void didUpdateWidget(MaterialApp 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);
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)
RectTween _createRectTween(Rect begin, Rect end) {
return new MaterialRectArcTween(begin: begin, end: end);
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
final String 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;
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 ??,
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(,
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,