app.dart 21.4 KB
Newer Older
1 2 3 4
// 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.

5
import 'package:flutter/rendering.dart';
6
import 'package:flutter/foundation.dart';
7
import 'package:flutter/widgets.dart';
8

9
import 'arc.dart';
10
import 'colors.dart';
11 12
import 'floating_action_button.dart';
import 'icons.dart';
13
import 'material_localizations.dart';
14
import 'page.dart';
15
import 'theme.dart';
16 17 18 19 20 21

const TextStyle _errorTextStyle = const TextStyle(
  color: const Color(0xD0FF0000),
  fontFamily: 'monospace',
  fontSize: 48.0,
  fontWeight: FontWeight.w900,
22
  decoration: TextDecoration.underline,
23
  decorationColor: const Color(0xFFFFFF00),
24 25 26
  decorationStyle: TextDecorationStyle.double
);

27 28 29
/// An application that uses material design.
///
/// A convenience widget that wraps a number of widgets that are commonly
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
/// 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.
53 54 55
///
/// See also:
///
56 57 58 59
///  * [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.
60
class MaterialApp extends StatefulWidget {
61 62
  /// Creates a MaterialApp.
  ///
63 64 65 66 67
  /// 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.
68
  ///
69
  /// This class creates an instance of [WidgetsApp].
70 71 72
  ///
  /// The boolean arguments, [routes], and [navigatorObservers], must not be null.
  MaterialApp({ // can't be const because the asserts use methods on Map :-(
73
    Key key,
74
    this.title: '',
75
    this.onGenerateTitle,
76
    this.color,
77 78 79
    this.theme,
    this.home,
    this.routes: const <String, WidgetBuilder>{},
80
    this.initialRoute,
81
    this.onGenerateRoute,
82
    this.onUnknownRoute,
83 84
    this.locale,
    this.localizationsDelegates,
85 86
    this.localeResolutionCallback,
    this.supportedLocales: const <Locale>[const Locale('en', 'US')],
87
    this.navigatorObservers: const <NavigatorObserver>[],
88
    this.debugShowMaterialGrid: false,
89
    this.showPerformanceOverlay: false,
90
    this.checkerboardRasterCacheImages: false,
91
    this.checkerboardOffscreenLayers: false,
92 93
    this.showSemanticsDebugger: false,
    this.debugShowCheckedModeBanner: true
94 95
  }) : assert(title != null),
       assert(routes != null),
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
       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.'
       ),
121
       super(key: key);
122

123 124 125 126 127 128 129 130 131 132
  /// 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].
133
  final String title;
134

135 136 137
  /// If non-null this function is called to produce the app's
  /// title string, otherwise [title] is used.
  ///
138
  /// The [onGenerateTitle] `context` parameter includes the [WidgetsApp]'s
139 140 141 142 143 144 145 146
  /// [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;

147
  /// The colors to use for the application's widgets.
148
  final ThemeData theme;
149

150 151
  /// The widget for the default route of the app ([Navigator.defaultRouteName],
  /// which is `/`).
152
  ///
153 154 155
  /// 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.
156
  ///
157 158 159
  /// 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].
160
  ///
161 162
  /// If [home] is specified, then [routes] must not include an entry for `/`,
  /// as [home] takes its place.
163 164
  final Widget home;

165 166 167 168 169 170 171
  /// 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;

172 173 174 175 176 177
  /// 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.
178 179 180
  ///
  /// If the app only has one page, then you can specify it using [home] instead.
  ///
181 182 183
  /// 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.
184
  ///
185 186 187
  /// 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.
Ian Hickson's avatar
Ian Hickson committed
188 189
  final Map<String, WidgetBuilder> routes;

190 191
  /// The name of the first route to show.
  ///
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
  /// 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.
210 211
  final String initialRoute;

212 213
  /// The route generator callback used when the app is navigated to a
  /// named route.
214 215 216 217 218 219 220 221 222 223
  ///
  /// 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.
224 225
  final RouteFactory onGenerateRoute;

226 227 228 229 230 231 232 233 234 235 236
  /// 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;

237 238 239 240 241 242 243 244 245
  /// 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.
246 247 248 249
  ///
  /// Delegates that produce [WidgetsLocalizations] and [MaterialLocalizations]
  /// are included automatically. Apps can provide their own versions of these
  /// localizations by creating implementations of
250
  /// [LocalizationsDelegate<WidgetsLocalizations>] or
251
  /// [LocalizationsDelegate<MaterialLocalizations>] whose load methods return
252
  /// custom versions of [WidgetsLocalizations] or [MaterialLocalizations].
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
  ///
  /// 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(),
  ///   ],
  ///   // ...
  /// )
  /// ```
301
  final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates;
302

303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
  /// 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
332
  /// `[const Locale('en', 'US')]`. It is passed along unmodified to the
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
  /// [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;

362
  /// Turns on a performance overlay.
363 364 365 366
  ///
  /// See also:
  ///
  ///  * <https://flutter.io/debugging/#performanceoverlay>
367 368
  final bool showPerformanceOverlay;

369 370 371
  /// Turns on checkerboarding of raster cache images.
  final bool checkerboardRasterCacheImages;

372 373 374
  /// Turns on checkerboarding of layers rendered to offscreen bitmaps.
  final bool checkerboardOffscreenLayers;

375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
  /// 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;

394 395 396
  /// The list of observers for the [Navigator] created for this app.
  final List<NavigatorObserver> navigatorObservers;

397
  /// Turns on a [GridPaper] overlay that paints a baseline grid
398 399
  /// Material apps.
  ///
400
  /// Only available in checked mode.
401 402 403 404
  ///
  /// See also:
  ///
  ///  * <https://material.google.com/layout/metrics-keylines.html>
Ian Hickson's avatar
Ian Hickson committed
405
  final bool debugShowMaterialGrid;
406

407
  @override
Adam Barth's avatar
Adam Barth committed
408
  _MaterialAppState createState() => new _MaterialAppState();
409 410
}

Adam Barth's avatar
Adam Barth committed
411
class _MaterialScrollBehavior extends ScrollBehavior {
412 413 414 415 416 417
  @override
  TargetPlatform getPlatform(BuildContext context) {
    return Theme.of(context).platform;
  }

  @override
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
  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;
433 434 435
  }
}

436
class _MaterialAppState extends State<MaterialApp> {
437 438 439 440 441 442 443 444
  HeroController _heroController;

  @override
  void initState() {
    super.initState();
    _heroController = new HeroController(createRectTween: _createRectTween);
  }

445
  // Combine the Localizations for Material with the ones contributed
446 447 448 449
  // 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.
450
  Iterable<LocalizationsDelegate<dynamic>> get _localizationsDelegates sync* {
451 452
    if (widget.localizationsDelegates != null)
      yield* widget.localizationsDelegates;
453
    yield DefaultMaterialLocalizations.delegate;
454 455
  }

456 457 458
  RectTween _createRectTween(Rect begin, Rect end) {
    return new MaterialRectArcTween(begin: begin, end: end);
  }
459

460
  Route<dynamic> _onGenerateRoute(RouteSettings settings) {
461 462 463
    final String name = settings.name;
    WidgetBuilder builder;
    if (name == Navigator.defaultRouteName && widget.home != null)
464
      builder = (BuildContext context) => widget.home;
465 466
    else
      builder = widget.routes[name];
467
    if (builder != null) {
468
      return new MaterialPageRoute<dynamic>(
469
        builder: builder,
470
        settings: settings,
471 472
      );
    }
473 474
    if (widget.onGenerateRoute != null)
      return widget.onGenerateRoute(settings);
475 476
    return null;
  }
477

478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
  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;
494
    }());
495 496 497 498 499 500 501 502 503 504 505
    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;
506
    }());
507 508 509
    return result;
  }

510
  @override
511
  Widget build(BuildContext context) {
512
    final ThemeData theme = widget.theme ?? new ThemeData.fallback();
513 514
    Widget result = new AnimatedTheme(
      data: theme,
515
      isMaterialAppTheme: true,
516
      child: new WidgetsApp(
517
        key: new GlobalObjectKey(this),
518
        title: widget.title,
519
        onGenerateTitle: widget.onGenerateTitle,
520
        textStyle: _errorTextStyle,
521
        // blue is the primary color of the default theme
522
        color: widget.color ?? theme?.primaryColor ?? Colors.blue,
523
        navigatorObservers:
524
            new List<NavigatorObserver>.from(widget.navigatorObservers)
525
              ..add(_heroController),
526
        initialRoute: widget.initialRoute,
527
        onGenerateRoute: _onGenerateRoute,
528
        onUnknownRoute: _onUnknownRoute,
529
        locale: widget.locale,
530
        localizationsDelegates: _localizationsDelegates,
531 532
        localeResolutionCallback: widget.localeResolutionCallback,
        supportedLocales: widget.supportedLocales,
533 534
        showPerformanceOverlay: widget.showPerformanceOverlay,
        checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
535
        checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
536
        showSemanticsDebugger: widget.showSemanticsDebugger,
537
        debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
538 539 540 541 542 543 544
        inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) {
          return new FloatingActionButton(
            child: const Icon(Icons.search),
            onPressed: onPressed,
            mini: true,
          );
        },
545
      )
546
    );
547

Ian Hickson's avatar
Ian Hickson committed
548
    assert(() {
549
      if (widget.debugShowMaterialGrid) {
Ian Hickson's avatar
Ian Hickson committed
550 551 552 553
        result = new GridPaper(
          color: const Color(0xE0F9BBE0),
          interval: 8.0,
          divisions: 2,
554 555
          subdivisions: 1,
          child: result,
Ian Hickson's avatar
Ian Hickson committed
556 557 558
        );
      }
      return true;
559
    }());
560

Adam Barth's avatar
Adam Barth committed
561
    return new ScrollConfiguration(
562
      behavior: new _MaterialScrollBehavior(),
563
      child: result,
564
    );
565
  }
566
}