blob: be3789b13b836297ff4980f0bdff30c12fa17c6e [file] [log] [blame]
// Copyright 2014 The Flutter 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:ui' as ui;
import 'package:flutter/cupertino.dart';
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',
);
/// Describes which theme will be used by [MaterialApp].
enum ThemeMode {
/// Use either the light or dark theme based on what the user has selected in
/// the system settings.
system,
/// Always use the light mode regardless of system preference.
light,
/// Always use the dark mode (if available) regardless of system preference.
dark,
}
/// 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.
///
/// {@tool snippet}
/// This example shows how to create a [MaterialApp] that disables the "debug"
/// banner with a [home] route that will be displayed when the app is launched.
///
/// ![The MaterialApp displays a Scaffold ](https://flutter.github.io/assets-for-api-docs/assets/material/basic_material_app.png)
///
/// ```dart
/// MaterialApp(
/// home: Scaffold(
/// appBar: AppBar(
/// title: const Text('Home'),
/// ),
/// ),
/// debugShowCheckedModeBanner: false,
/// )
/// ```
/// {@end-tool}
///
/// {@tool snippet}
/// This example shows how to create a [MaterialApp] that uses the [routes]
/// `Map` to define the "home" route and an "about" route.
///
/// ```dart
/// MaterialApp(
/// routes: <String, WidgetBuilder>{
/// '/': (BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: const Text('Home Route'),
/// ),
/// );
/// },
/// '/about': (BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: const Text('About Route'),
/// ),
/// );
/// }
/// },
/// )
/// ```
/// {@end-tool}
///
/// {@tool snippet}
/// This example shows how to create a [MaterialApp] that defines a [theme] that
/// will be used for material widgets in the app.
///
/// ![The MaterialApp displays a Scaffold with a dark background and a blue / grey AppBar at the top](https://flutter.github.io/assets-for-api-docs/assets/material/theme_material_app.png)
///
/// ```dart
/// MaterialApp(
/// theme: ThemeData(
/// brightness: Brightness.dark,
/// primaryColor: Colors.blueGrey
/// ),
/// home: Scaffold(
/// appBar: AppBar(
/// title: const Text('MaterialApp Theme'),
/// ),
/// ),
/// )
/// ```
/// {@end-tool}
///
/// 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.
/// * The Flutter Internationalization Tutorial,
/// <https://flutter.dev/tutorials/internationalization/>.
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.
const MaterialApp({
Key key,
this.navigatorKey,
this.home,
this.routes = const <String, WidgetBuilder>{},
this.initialRoute,
this.onGenerateRoute,
this.onGenerateInitialRoutes,
this.onUnknownRoute,
this.navigatorObservers = const <NavigatorObserver>[],
this.builder,
this.title = '',
this.onGenerateTitle,
this.color,
this.theme,
this.darkTheme,
this.themeMode = ThemeMode.system,
this.locale,
this.localizationsDelegates,
this.localeListResolutionCallback,
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,
this.shortcuts,
this.actions,
}) : assert(routes != null),
assert(navigatorObservers != null),
assert(title != null),
assert(debugShowMaterialGrid != null),
assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null),
assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null),
super(key: key);
/// {@macro flutter.widgets.widgetsApp.navigatorKey}
final GlobalKey<NavigatorState> navigatorKey;
/// {@macro flutter.widgets.widgetsApp.home}
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.
///
/// {@macro flutter.widgets.widgetsApp.routes}
final Map<String, WidgetBuilder> routes;
/// {@macro flutter.widgets.widgetsApp.initialRoute}
final String initialRoute;
/// {@macro flutter.widgets.widgetsApp.onGenerateRoute}
final RouteFactory onGenerateRoute;
/// {@macro flutter.widgets.widgetsApp.onGenerateInitialRoutes}
final InitialRouteListFactory onGenerateInitialRoutes;
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
final RouteFactory onUnknownRoute;
/// {@macro flutter.widgets.widgetsApp.navigatorObservers}
final List<NavigatorObserver> navigatorObservers;
/// {@macro flutter.widgets.widgetsApp.builder}
///
/// Material specific features such as [showDialog] and [showMenu], and widgets
/// such as [Tooltip], [PopupMenuButton], also require a [Navigator] to properly
/// 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;
/// Default visual properties, like colors fonts and shapes, for this app's
/// material widgets.
///
/// A second [darkTheme] [ThemeData] value, which is used to provide a dark
/// version of the user interface can also be specified. [themeMode] will
/// control which theme will be used if a [darkTheme] is provided.
///
/// The default value of this property is the value of [ThemeData.light()].
///
/// See also:
///
/// * [themeMode], which controls which theme to use.
/// * [MediaQueryData.platformBrightness], which indicates the platform's
/// desired brightness and is used to automatically toggle between [theme]
/// and [darkTheme] in [MaterialApp].
/// * [ThemeData.brightness], which indicates the [Brightness] of a theme's
/// colors.
final ThemeData theme;
/// The [ThemeData] to use when a 'dark mode' is requested by the system.
///
/// Some host platforms allow the users to select a system-wide 'dark mode',
/// or the application may want to offer the user the ability to choose a
/// dark theme just for this application. This is theme that will be used for
/// such cases. [themeMode] will control which theme will be used.
///
/// This theme should have a [ThemeData.brightness] set to [Brightness.dark].
///
/// Uses [theme] instead when null. Defaults to the value of
/// [ThemeData.light()] when both [darkTheme] and [theme] are null.
///
/// See also:
///
/// * [themeMode], which controls which theme to use.
/// * [MediaQueryData.platformBrightness], which indicates the platform's
/// desired brightness and is used to automatically toggle between [theme]
/// and [darkTheme] in [MaterialApp].
/// * [ThemeData.brightness], which is typically set to the value of
/// [MediaQueryData.platformBrightness].
final ThemeData darkTheme;
/// Determines which theme will be used by the application if both [theme]
/// and [darkTheme] are provided.
///
/// If set to [ThemeMode.system], the choice of which theme to use will
/// be based on the user's system preferences. If the [MediaQuery.platformBrightnessOf]
/// is [Brightness.light], [theme] will be used. If it is [Brightness.dark],
/// [darkTheme] will be used (unless it is [null], in which case [theme]
/// will be used.
///
/// If set to [ThemeMode.light] the [theme] will always be used,
/// regardless of the user's system preference.
///
/// If set to [ThemeMode.dark] the [darkTheme] will be used
/// regardless of the user's system preference. If [darkTheme] is [null]
/// then it will fallback to using [theme].
///
/// The default value is [ThemeMode.system].
///
/// See also:
///
/// * [theme], which is used when a light mode is selected.
/// * [darkTheme], which is used when a dark mode is selected.
/// * [ThemeData.brightness], which indicates to various parts of the
/// system what kind of theme is being used.
final ThemeMode themeMode;
/// {@macro flutter.widgets.widgetsApp.color}
final Color color;
/// {@macro flutter.widgets.widgetsApp.locale}
final Locale locale;
/// {@macro flutter.widgets.widgetsApp.localizationsDelegates}
///
/// Internationalized apps that require translations for one of the locales
/// listed in [GlobalMaterialLocalizations] should specify this parameter
/// and list the [supportedLocales] that the application can handle.
///
/// ```dart
/// import 'package:flutter_localizations/flutter_localizations.dart';
/// MaterialApp(
/// localizationsDelegates: [
/// // ... app-specific localization delegate[s] here
/// GlobalMaterialLocalizations.delegate,
/// GlobalWidgetsLocalizations.delegate,
/// ],
/// supportedLocales: [
/// const Locale('en', 'US'), // English
/// const Locale('he', 'IL'), // Hebrew
/// // ... other locales the app supports
/// ],
/// // ...
/// )
/// ```
///
/// ## Adding localizations for a new locale
///
/// The information that follows applies to the unusual case of an app
/// adding translations for a language not already supported by
/// [GlobalMaterialLocalizations].
///
/// 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 SynchronousFuture(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
/// MaterialApp(
/// localizationsDelegates: [
/// const FooLocalizationsDelegate(),
/// ],
/// // ...
/// )
/// ```
/// See also:
///
/// * [supportedLocales], which must be specified along with
/// [localizationsDelegates].
/// * [GlobalMaterialLocalizations], a [localizationsDelegates] value
/// which provides material localizations for many languages.
/// * The Flutter Internationalization Tutorial,
/// <https://flutter.dev/tutorials/internationalization/>.
final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates;
/// {@macro flutter.widgets.widgetsApp.localeListResolutionCallback}
///
/// This callback is passed along to the [WidgetsApp] built by this widget.
final LocaleListResolutionCallback localeListResolutionCallback;
/// {@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.
///
/// See also:
///
/// * [localizationsDelegates], which must be specified for localized
/// applications.
/// * [GlobalMaterialLocalizations], a [localizationsDelegates] value
/// which provides material localizations for many languages.
/// * The Flutter Internationalization Tutorial,
/// <https://flutter.dev/tutorials/internationalization/>.
final Iterable<Locale> supportedLocales;
/// Turns on a performance overlay.
///
/// See also:
///
/// * <https://flutter.dev/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;
/// {@macro flutter.widgets.widgetsApp.shortcuts}
/// {@tool snippet}
/// This example shows how to add a single shortcut for
/// [LogicalKeyboardKey.select] to the default shortcuts without needing to
/// add your own [Shortcuts] widget.
///
/// Alternatively, you could insert a [Shortcuts] widget with just the mapping
/// you want to add between the [WidgetsApp] and its child and get the same
/// effect.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return WidgetsApp(
/// shortcuts: <LogicalKeySet, Intent>{
/// ... WidgetsApp.defaultShortcuts,
/// LogicalKeySet(LogicalKeyboardKey.select): const Intent(ActivateAction.key),
/// },
/// color: const Color(0xFFFF0000),
/// builder: (BuildContext context, Widget child) {
/// return const Placeholder();
/// },
/// );
/// }
/// ```
/// {@end-tool}
/// {@macro flutter.widgets.widgetsApp.shortcuts.seeAlso}
final Map<LogicalKeySet, Intent> shortcuts;
/// {@macro flutter.widgets.widgetsApp.actions}
/// {@tool snippet}
/// This example shows how to add a single action handling an
/// [ActivateAction] to the default actions without needing to
/// add your own [Actions] widget.
///
/// Alternatively, you could insert a [Actions] widget with just the mapping
/// you want to add between the [WidgetsApp] and its child and get the same
/// effect.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return WidgetsApp(
/// actions: <LocalKey, ActionFactory>{
/// ... WidgetsApp.defaultActions,
/// ActivateAction.key: () => CallbackAction(
/// ActivateAction.key,
/// onInvoke: (FocusNode focusNode, Intent intent) {
/// // Do something here...
/// },
/// ),
/// },
/// color: const Color(0xFFFF0000),
/// builder: (BuildContext context, Widget child) {
/// return const Placeholder();
/// },
/// );
/// }
/// ```
/// {@end-tool}
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
final Map<LocalKey, ActionFactory> actions;
/// Turns on a [GridPaper] overlay that paints a baseline grid
/// Material apps.
///
/// Only available in checked mode.
///
/// See also:
///
/// * <https://material.io/design/layout/spacing-methods.html>
final bool debugShowMaterialGrid;
@override
_MaterialAppState createState() => _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:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return child;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return 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 = 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 = HeroController(createRectTween: _createRectTween);
}
_updateNavigator();
}
List<NavigatorObserver> _navigatorObservers;
void _updateNavigator() {
if (widget.home != null ||
widget.routes.isNotEmpty ||
widget.onGenerateRoute != null ||
widget.onUnknownRoute != null) {
_navigatorObservers = List<NavigatorObserver>.from(widget.navigatorObservers)
..add(_heroController);
} else {
_navigatorObservers = const <NavigatorObserver>[];
}
}
RectTween _createRectTween(Rect begin, Rect end) {
return MaterialRectArcTween(begin: begin, end: end);
}
// 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;
yield DefaultCupertinoLocalizations.delegate;
}
@override
Widget build(BuildContext context) {
Widget result = WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: _navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
return MaterialPageRoute<T>(settings: settings, builder: builder);
},
home: widget.home,
routes: widget.routes,
initialRoute: widget.initialRoute,
onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute,
builder: (BuildContext context, Widget child) {
// Use a light theme, dark theme, or fallback theme.
final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
ThemeData theme;
if (widget.darkTheme != null) {
final ui.Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);
if (mode == ThemeMode.dark ||
(mode == ThemeMode.system && platformBrightness == ui.Brightness.dark)) {
theme = widget.darkTheme;
}
}
theme ??= widget.theme ?? ThemeData.fallback();
return AnimatedTheme(
data: theme,
isMaterialAppTheme: true,
child: widget.builder != null
? Builder(
builder: (BuildContext context) {
// Why are we surrounding a builder with a builder?
//
// The widget.builder may contain code that invokes
// Theme.of(), which should return the theme we selected
// above in AnimatedTheme. However, if we invoke
// widget.builder() directly as the child of AnimatedTheme
// then there is no Context separating them, and the
// widget.builder() will not find the theme. Therefore, we
// surround widget.builder with yet another builder so that
// a context separates them and Theme.of() correctly
// resolves to the theme we passed to AnimatedTheme.
return widget.builder(context, child);
},
)
: child,
);
},
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: _errorTextStyle,
// The color property is always pulled from the light theme, even if dark
// mode is activated. This was done to simplify the technical details
// of switching themes and it was deemed acceptable because this color
// property is only used on old Android OSes to color the app bar in
// Android's switcher UI.
//
// blue is the primary color of the default theme
color: widget.color ?? widget.theme?.primaryColor ?? Colors.blue,
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
localeListResolutionCallback: widget.localeListResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) {
return FloatingActionButton(
child: const Icon(Icons.search),
onPressed: onPressed,
mini: true,
);
},
shortcuts: widget.shortcuts,
actions: widget.actions,
);
assert(() {
if (widget.debugShowMaterialGrid) {
result = GridPaper(
color: const Color(0xE0F9BBE0),
interval: 8.0,
divisions: 2,
subdivisions: 1,
child: result,
);
}
return true;
}());
return ScrollConfiguration(
behavior: _MaterialScrollBehavior(),
child: result,
);
}
}