| // 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 'dart:ui' as ui show window; |
| |
| import 'package:flutter/rendering.dart'; |
| |
| import 'banner.dart'; |
| import 'basic.dart'; |
| import 'binding.dart'; |
| import 'framework.dart'; |
| import 'localizations.dart'; |
| import 'media_query.dart'; |
| import 'navigator.dart'; |
| import 'performance_overlay.dart'; |
| import 'semantics_debugger.dart'; |
| import 'text.dart'; |
| import 'title.dart'; |
| import 'widget_inspector.dart'; |
| |
| export 'dart:ui' show Locale; |
| |
| /// The signature of [WidgetsApp.localeResolutionCallback]. |
| /// |
| /// A `LocaleResolutionCallback` is responsible for computing the locale of the app's |
| /// [Localizations] object when the app starts and when user changes the default |
| /// locale for the device. |
| /// |
| /// The `locale` is the device's locale when the app started, or the device |
| /// locale the user selected after the app was started. The `supportedLocales` |
| /// parameter is just the value of [WidgetsApp.supportedLocales]. |
| typedef Locale LocaleResolutionCallback(Locale locale, Iterable<Locale> supportedLocales); |
| |
| /// The signature of [WidgetsApp.onGenerateTitle]. |
| /// |
| /// Used to generate a value for the app's [Title.title], which the device uses |
| /// to identify the app for the user. The `context` includes the [WidgetsApp]'s |
| /// [Localizations] widget so that this method can be used to produce a |
| /// localized title. |
| /// |
| /// This function must not return null. |
| typedef String GenerateAppTitle(BuildContext context); |
| |
| /// A convenience class that wraps a number of widgets that are commonly |
| /// required for an application. |
| /// |
| /// One of the primary roles that [WidgetsApp] provides is binding the system |
| /// back button to popping the [Navigator] or quitting the application. |
| /// |
| /// See also: [CheckedModeBanner], [DefaultTextStyle], [MediaQuery], |
| /// [Localizations], [Title], [Navigator], [Overlay], [SemanticsDebugger] (the |
| /// widgets wrapped by this one). |
| class WidgetsApp extends StatefulWidget { |
| /// Creates a widget that wraps a number of widgets that are commonly |
| /// required for an application. |
| /// |
| /// The boolean arguments, [color], and [navigatorObservers] must not be null. |
| /// |
| /// If the [builder] is null, the [onGenerateRoute] argument is required, and |
| /// corresponds to [Navigator.onGenerateRoute]. If the [builder] is non-null |
| /// and the [onGenerateRoute] argument is null, then the [builder] will not be |
| /// provided with a [Navigator]. If [onGenerateRoute] is not provided, |
| /// [navigatorKey], [onUnknownRoute], [navigatorObservers], and [initialRoute] |
| /// must have their default values, as they will have no effect. |
| /// |
| /// The `supportedLocales` argument must be a list of one or more elements. |
| /// By default supportedLocales is `[const Locale('en', 'US')]`. |
| WidgetsApp({ // can't be const because the asserts use methods on Iterable :-( |
| Key key, |
| this.navigatorKey, |
| this.onGenerateRoute, |
| this.onUnknownRoute, |
| this.navigatorObservers: const <NavigatorObserver>[], |
| this.initialRoute, |
| this.builder, |
| this.title: '', |
| this.onGenerateTitle, |
| this.textStyle, |
| @required this.color, |
| this.locale, |
| this.localizationsDelegates, |
| this.localeResolutionCallback, |
| this.supportedLocales: const <Locale>[const Locale('en', 'US')], |
| this.showPerformanceOverlay: false, |
| this.checkerboardRasterCacheImages: false, |
| this.checkerboardOffscreenLayers: false, |
| this.showSemanticsDebugger: false, |
| this.debugShowWidgetInspector: false, |
| this.debugShowCheckedModeBanner: true, |
| this.inspectorSelectButtonBuilder, |
| }) : assert(navigatorObservers != null), |
| assert(onGenerateRoute != null || navigatorKey == null), |
| assert(onGenerateRoute != null || onUnknownRoute == null), |
| assert(onGenerateRoute != null || navigatorObservers == const <NavigatorObserver>[]), |
| assert(onGenerateRoute != null || initialRoute == null), |
| assert(onGenerateRoute != null || builder != null), |
| assert(title != null), |
| assert(color != null), |
| assert(supportedLocales != null && supportedLocales.isNotEmpty), |
| assert(showPerformanceOverlay != null), |
| assert(checkerboardRasterCacheImages != null), |
| assert(checkerboardOffscreenLayers != null), |
| assert(showSemanticsDebugger != null), |
| assert(debugShowCheckedModeBanner != null), |
| assert(debugShowWidgetInspector != 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 [onGenerateRoute] is not null; if it is |
| /// null, [navigatorKey] must also be null. |
| final GlobalKey<NavigatorState> navigatorKey; |
| |
| /// The route generator callback used when the app is navigated to a |
| /// named 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. |
| /// |
| /// The [Navigator] is only built if [onGenerateRoute] is not null. If |
| /// [onGenerateRoute] is null, the [builder] must be non-null. |
| final RouteFactory onGenerateRoute; |
| |
| /// Called when [onGenerateRoute] fails to generate a route. |
| /// |
| /// 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. |
| /// |
| /// Unknown routes can arise either from errors in the app or from external |
| /// requests to push routes, such as from Android intents. |
| /// |
| /// The [Navigator] is only built if [onGenerateRoute] is not null; if it is |
| /// null, [onUnknownRoute] must also be null. |
| final RouteFactory onUnknownRoute; |
| |
| /// 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. |
| /// |
| /// The [Navigator] is only built if [onGenerateRoute] is not null; if it is |
| /// null, [initialRoute] must also 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; |
| |
| /// The list of observers for the [Navigator] created for this app. |
| /// |
| /// This list must be replaced by a list of newly-created observers if the |
| /// [navigatorKey] is changed. |
| /// |
| /// The [Navigator] is only built if [onGenerateRoute] is not null; if it is |
| /// null, [navigatorObservers] must be left to its default value, the empty |
| /// list. |
| final List<NavigatorObserver> navigatorObservers; |
| |
| /// A builder for inserting widgets above the [Navigator] but below the other |
| /// widgets created by the [WidgetsApp] widget, or for replacing the |
| /// [Navigator] entirely. |
| /// |
| /// For example, from the [BuildContext] passed to this method, the |
| /// [Directionality], [Localizations], [DefaultTextStyle], [MediaQuery], etc, |
| /// are all available. They can also be overridden in a way that impacts all |
| /// the routes in the [Navigator]. |
| /// |
| /// This is rarely useful, but can be used in applications that wish to |
| /// override those defaults, e.g. to force the application into right-to-left |
| /// mode despite being in English, or to override the [MediaQuery] metrics |
| /// (e.g. to leave a gap for advertisements shown by a plugin from OEM code). |
| /// |
| /// The [builder] callback is passed two arguments, the [BuildContext] (as |
| /// `context`) and a [Navigator] widget (as `child`). |
| /// |
| /// If [onGenerateRoute] is null, the `child` will be null, and it is the |
| /// responsibility of the [builder] to provide the application's routing |
| /// machinery. |
| /// |
| /// If [onGenerateRoute] is not null, 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], [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. At least one of either [onGenerateRoute] or |
| /// [builder] must be non-null. |
| /// |
| /// For specifically overriding the [title] with a value based on the |
| /// [Localizations], consider [onGenerateTitle] instead. |
| final TransitionBuilder builder; |
| |
| /// 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]. |
| final String title; |
| |
| /// If non-null this callback 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. |
| /// |
| /// The [onGenerateTitle] callback is called each time the [WidgetsApp] |
| /// rebuilds. |
| final GenerateAppTitle onGenerateTitle; |
| |
| /// The default text style for [Text] in the application. |
| final TextStyle textStyle; |
| |
| /// 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 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. |
| 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]. |
| /// |
| /// If the callback is null or if it returns 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 locale in [supportedLocales]. |
| /// |
| /// See also: |
| /// |
| /// * [MaterialApp.localeResolutionCallback], which sets the callback of the |
| /// [WidgetsApp] it creates. |
| 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. Its default value is just |
| /// `[const Locale('en', 'US')]`. |
| /// |
| /// 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]. |
| /// |
| /// See also: |
| /// |
| /// * [MaterialApp.supportedLocales], which sets the `supportedLocales` |
| /// of the [WidgetsApp] it creates. |
| /// |
| /// * [localeResolutionCallback], an app callback that resolves the app's locale |
| /// when the device's locale changes. |
| /// |
| /// * [localizationsDelegates], which collectively define all of the localized |
| /// resources used by this app. |
| final Iterable<Locale> supportedLocales; |
| |
| /// Turns on a performance overlay. |
| /// https://flutter.io/debugging/#performanceoverlay |
| final bool showPerformanceOverlay; |
| |
| /// Checkerboards raster cache images. |
| /// |
| /// See [PerformanceOverlay.checkerboardRasterCacheImages]. |
| final bool checkerboardRasterCacheImages; |
| |
| /// Checkerboards layers rendered to offscreen bitmaps. |
| /// |
| /// See [PerformanceOverlay.checkerboardOffscreenLayers]. |
| final bool checkerboardOffscreenLayers; |
| |
| /// Turns on an overlay that shows the accessibility information |
| /// reported by the framework. |
| final bool showSemanticsDebugger; |
| |
| /// Turns on an overlay that enables inspecting the widget tree. |
| /// |
| /// The inspector is only available in checked mode as it depends on |
| /// [RenderObject.debugDescribeChildren] which should not be called outside of |
| /// checked mode. |
| final bool debugShowWidgetInspector; |
| |
| /// Builds the widget the [WidgetInspector] uses to switch between view and |
| /// inspect modes. |
| /// |
| /// This lets [MaterialApp] to use a material button to toggle the inspector |
| /// select mode without requiring [WidgetInspector] to depend on the |
| /// material package. |
| final InspectorSelectButtonBuilder inspectorSelectButtonBuilder; |
| |
| /// Turns on a "DEBUG" little 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 avoid people 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; |
| |
| /// If true, forces the performance overlay to be visible in all instances. |
| /// |
| /// Used by the `showPerformanceOverlay` observatory extension. |
| static bool showPerformanceOverlayOverride = false; |
| |
| /// If true, forces the widget inspector to be visible. |
| /// |
| /// Used by the `debugShowWidgetInspector` debugging extension. |
| /// |
| /// The inspector allows you to select a location on your device or emulator |
| /// and view what widgets and render objects associated with it. An outline of |
| /// the selected widget and some summary information is shown on device and |
| /// more detailed information is shown in the IDE or Observatory. |
| static bool debugShowWidgetInspectorOverride = false; |
| |
| /// If false, prevents the debug banner from being visible. |
| /// |
| /// Used by the `debugAllowBanner` observatory extension. |
| /// |
| /// This is how `flutter run` turns off the banner when you take a screen shot |
| /// with "s". |
| static bool debugAllowBannerOverride = true; |
| |
| @override |
| _WidgetsAppState createState() => new _WidgetsAppState(); |
| } |
| |
| class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserver { |
| |
| // STATE LIFECYCLE |
| |
| @override |
| void initState() { |
| super.initState(); |
| _updateNavigator(); |
| _locale = _resolveLocale(ui.window.locale, widget.supportedLocales); |
| WidgetsBinding.instance.addObserver(this); |
| } |
| |
| @override |
| void didUpdateWidget(WidgetsApp oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (widget.navigatorKey != oldWidget.navigatorKey) |
| _updateNavigator(); |
| } |
| |
| @override |
| void dispose() { |
| WidgetsBinding.instance.removeObserver(this); |
| super.dispose(); |
| } |
| |
| @override |
| void didChangeAppLifecycleState(AppLifecycleState state) { } |
| |
| @override |
| void didHaveMemoryPressure() { } |
| |
| |
| // NAVIGATOR |
| |
| GlobalKey<NavigatorState> _navigator; |
| |
| void _updateNavigator() { |
| if (widget.onGenerateRoute == null) { |
| _navigator = null; |
| } else { |
| _navigator = widget.navigatorKey ?? new GlobalObjectKey<NavigatorState>(this); |
| } |
| } |
| |
| // On Android: the user has pressed the back button. |
| @override |
| Future<bool> didPopRoute() async { |
| assert(mounted); |
| final NavigatorState navigator = _navigator?.currentState; |
| if (navigator == null) |
| return false; |
| return await navigator.maybePop(); |
| } |
| |
| @override |
| Future<bool> didPushRoute(String route) async { |
| assert(mounted); |
| final NavigatorState navigator = _navigator?.currentState; |
| if (navigator == null) |
| return false; |
| navigator.pushNamed(route); |
| return true; |
| } |
| |
| |
| // LOCALIZATION |
| |
| Locale _locale; |
| |
| Locale _resolveLocale(Locale newLocale, Iterable<Locale> supportedLocales) { |
| if (widget.localeResolutionCallback != null) { |
| final Locale locale = widget.localeResolutionCallback(newLocale, widget.supportedLocales); |
| if (locale != null) |
| return locale; |
| } |
| |
| Locale matchesLanguageCode; |
| for (Locale locale in supportedLocales) { |
| if (locale == newLocale) |
| return newLocale; |
| if (locale.languageCode == newLocale.languageCode) |
| matchesLanguageCode ??= locale; |
| } |
| return matchesLanguageCode ?? supportedLocales.first; |
| } |
| |
| @override |
| void didChangeLocale(Locale locale) { |
| if (locale == _locale) |
| return; |
| final Locale newLocale = _resolveLocale(locale, widget.supportedLocales); |
| if (newLocale != _locale) { |
| setState(() { |
| _locale = newLocale; |
| }); |
| } |
| } |
| |
| // Combine the Localizations for Widgets 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 |
| // WidgetsLocalizations.delegate. |
| Iterable<LocalizationsDelegate<dynamic>> get _localizationsDelegates sync* { |
| if (widget.localizationsDelegates != null) |
| yield* widget.localizationsDelegates; |
| yield DefaultWidgetsLocalizations.delegate; |
| } |
| |
| |
| // METRICS |
| |
| @override |
| void didChangeMetrics() { |
| setState(() { |
| // The properties of ui.window have changed. We use them in our build |
| // function, so we need setState(), but we don't cache anything locally. |
| }); |
| } |
| |
| @override |
| void didChangeTextScaleFactor() { |
| setState(() { |
| // The textScaleFactor property of ui.window has changed. We reference |
| // ui.window in our build function, so we need to call setState(), but |
| // we don't need to cache anything locally. |
| }); |
| } |
| |
| |
| // BUILDER |
| |
| @override |
| Widget build(BuildContext context) { |
| Widget navigator; |
| if (_navigator != null) { |
| navigator = new Navigator( |
| key: _navigator, |
| initialRoute: widget.initialRoute ?? ui.window.defaultRouteName, |
| onGenerateRoute: widget.onGenerateRoute, |
| onUnknownRoute: widget.onUnknownRoute, |
| observers: widget.navigatorObservers, |
| ); |
| } |
| |
| Widget result; |
| if (widget.builder != null) { |
| result = new Builder( |
| builder: (BuildContext context) { |
| return widget.builder(context, navigator); |
| }, |
| ); |
| } else { |
| assert(navigator != null); |
| result = navigator; |
| } |
| |
| if (widget.textStyle != null) { |
| result = new DefaultTextStyle( |
| style: widget.textStyle, |
| child: result, |
| ); |
| } |
| |
| PerformanceOverlay performanceOverlay; |
| // We need to push a performance overlay if any of the display or checkerboarding |
| // options are set. |
| if (widget.showPerformanceOverlay || WidgetsApp.showPerformanceOverlayOverride) { |
| performanceOverlay = new PerformanceOverlay.allEnabled( |
| checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, |
| checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, |
| ); |
| } else if (widget.checkerboardRasterCacheImages || widget.checkerboardOffscreenLayers) { |
| performanceOverlay = new PerformanceOverlay( |
| checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, |
| checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, |
| ); |
| } |
| if (performanceOverlay != null) { |
| result = new Stack( |
| children: <Widget>[ |
| result, |
| new Positioned(top: 0.0, left: 0.0, right: 0.0, child: performanceOverlay), |
| ] |
| ); |
| } |
| |
| if (widget.showSemanticsDebugger) { |
| result = new SemanticsDebugger( |
| child: result, |
| ); |
| } |
| |
| assert(() { |
| if (widget.debugShowWidgetInspector || WidgetsApp.debugShowWidgetInspectorOverride) { |
| result = new WidgetInspector( |
| child: result, |
| selectButtonBuilder: widget.inspectorSelectButtonBuilder, |
| ); |
| } |
| if (widget.debugShowCheckedModeBanner && WidgetsApp.debugAllowBannerOverride) { |
| result = new CheckedModeBanner( |
| child: result, |
| ); |
| } |
| return true; |
| }()); |
| |
| Widget title; |
| if (widget.onGenerateTitle != null) { |
| title = new Builder( |
| // This Builder exists to provide a context below the Localizations widget. |
| // The onGenerateTitle callback can refer to Localizations via its context |
| // parameter. |
| builder: (BuildContext context) { |
| final String title = widget.onGenerateTitle(context); |
| assert(title != null, 'onGenerateTitle must return a non-null String'); |
| return new Title( |
| title: title, |
| color: widget.color, |
| child: result, |
| ); |
| }, |
| ); |
| } else { |
| title = new Title( |
| title: widget.title, |
| color: widget.color, |
| child: result, |
| ); |
| } |
| |
| return new MediaQuery( |
| data: new MediaQueryData.fromWindow(ui.window), |
| child: new Localizations( |
| locale: widget.locale ?? _locale, |
| delegates: _localizationsDelegates.toList(), |
| child: title, |
| ), |
| ); |
| } |
| } |