diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 142e76e63f..c899dfe568 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -92,33 +92,45 @@ enum MaterialTapTargetSize { shrinkWrap, } -/// Holds the color and typography values for a material design theme. +/// Defines the configuration of the overall visual [Theme] for a [MaterialApp] +/// or a widget subtree within the app. /// -/// Use this class to configure a [Theme] or [MaterialApp] widget. +/// The [MaterialApp] theme property can be used to configure the appearance +/// of the entire app. Widget subtree's within an app can override the app's +/// theme by including a [Theme] widget at the top of the subtree. /// -/// To obtain the current theme, use [Theme.of]. +/// Widgets whose appearance should align with the overall theme can obtain the +/// current theme's configuration with [Theme.of]. Material components typically +/// depend exclusively on the [colorScheme] and [textTheme]. These properties +/// are guaranteed to have non-null values. +/// +/// The static [Theme.of] method finds the [ThemeData] value specified for the +/// nearest [BuildContext] ancestor. This lookup is inexpensive, essentially +/// just a single HashMap access. It can sometimes be a little confusing +/// because [Theme.of] can not see a [Theme] widget that is defined in the +/// current build method's context. To overcome that, create a new custom widget +/// for the subtree that appears below the new [Theme], or insert a widget +/// that creates a new BuildContext, like [Builder]. /// /// {@tool snippet} -/// -/// This sample creates a [Theme] widget that stores the `ThemeData`. The -/// `ThemeData` can be accessed by descendant Widgets that use the correct -/// `context`. This example uses the [Builder] widget to gain access to a -/// descendant `context` that contains the `ThemeData`. -/// -/// The [Container] widget uses [Theme.of] to retrieve the [primaryColor] from -/// the `ThemeData` to draw an amber square. +/// In this example, the [Container] widget uses [Theme.of] to retrieve the +/// primary color from the theme's [colorScheme] to draw an amber square. +/// The [Builder] widget separates the parent theme's [BuildContext] from the +/// child's [BuildContext]. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/theme_data.png) /// /// ```dart /// Theme( -/// data: ThemeData(primaryColor: Colors.amber), +/// data: ThemeData.from( +/// colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.amber), +/// ), /// child: Builder( /// builder: (BuildContext context) { /// return Container( /// width: 100, /// height: 100, -/// color: Theme.of(context).primaryColor, +/// color: Theme.of(context).colorScheme.primary, /// ); /// }, /// ), @@ -126,10 +138,6 @@ enum MaterialTapTargetSize { /// ``` /// {@end-tool} /// -/// In addition to using the [Theme] widget, you can provide `ThemeData` to a -/// [MaterialApp]. The `ThemeData` will be used throughout the app to style -/// material design widgets. -/// /// {@tool snippet} /// /// This sample creates a [MaterialApp] widget that stores `ThemeData` and @@ -164,42 +172,33 @@ enum MaterialTapTargetSize { /// ) /// ``` /// {@end-tool} +/// +/// See for +/// more discussion on how to pick the right colors. + @immutable class ThemeData with Diagnosticable { - /// Create a [ThemeData] given a set of preferred values. + /// Create a [ThemeData] that's used to configure a [Theme]. /// - /// Default values will be derived for arguments that are omitted. + /// Typically, only the [brightness], [primaryColor], or [primarySwatch] are + /// specified. That pair of values are used to construct the [colorScheme]. /// - /// The most useful values to give are, in order of importance: + /// The [colorScheme] and [textTheme] are used by the Material components to + /// compute default values for visual properties. The API documentation for + /// each component widget explains exactly how the defaults are computed. /// - /// * The desired theme [brightness]. + /// The [textTheme] [TextStyle] colors are black if the color scheme's + /// brightness is [Brightness.light], and white for [Brightness.dark]. /// - /// * The primary color palette (the [primarySwatch]), chosen from - /// one of the swatches defined by the material design spec. This - /// should be one of the maps from the [Colors] class that do not - /// have "accent" in their name. + /// To override the appearance of specific components, provide + /// a component theme parameter like [sliderTheme], [toggleButtonsTheme], + /// or [bottomNavigationBarTheme]. /// - /// * The [accentColor], sometimes called the secondary color, and, - /// if the accent color is specified, its brightness - /// ([accentColorBrightness]), so that the right contrasting text - /// color will be used over the accent color. + /// See also: /// - /// Most of these parameters map to the [ThemeData] field with the same name, - /// all of which are described in more detail on the fields themselves. The - /// exceptions are: - /// - /// * [primarySwatch] - used to configure default values for several fields, - /// including: [primaryColor], [primaryColorBrightness], [primaryColorLight], - /// [primaryColorDark], [toggleableActiveColor], [accentColor], [colorScheme], - /// [secondaryHeaderColor], [textSelectionColor], [backgroundColor], and - /// [buttonColor]. - /// - /// * [fontFamily] - sets the default fontFamily for any - /// [TextStyle.fontFamily] that isn't set directly in the [textTheme], - /// [primaryTextTheme], or [accentTextTheme]. - /// - /// See for - /// more discussion on how to pick the right colors. + /// * [ThemeData.from], which creates a ThemeData from a [ColorScheme]. + /// * [ThemeData.light], which creates a light blue theme. + /// * [ThemeData.dark], which creates dark theme with a teal secondary [ColorScheme] color. factory ThemeData({ Brightness brightness, VisualDensity visualDensity, @@ -270,8 +269,9 @@ class ThemeData with Diagnosticable { BottomNavigationBarThemeData bottomNavigationBarTheme, bool fixTextFieldOutlineLabel, }) { - brightness ??= Brightness.light; - final bool isDark = brightness == Brightness.dark; + assert(colorScheme?.brightness == null || brightness == null || colorScheme.brightness == brightness); + final Brightness _brightness = brightness ?? colorScheme?.brightness ?? Brightness.light; + final bool isDark = _brightness == Brightness.dark; visualDensity ??= const VisualDensity(); primarySwatch ??= Colors.blue; primaryColor ??= isDark ? Colors.grey[900] : primarySwatch; @@ -298,7 +298,7 @@ class ThemeData with Diagnosticable { cardColor: cardColor, backgroundColor: backgroundColor, errorColor: errorColor, - brightness: brightness, + brightness: _brightness, ); splashFactory ??= InkSplash.splashFactory; @@ -364,7 +364,7 @@ class ThemeData with Diagnosticable { cardTheme ??= const CardTheme(); chipTheme ??= ChipThemeData.fromDefaults( secondaryColor: primaryColor, - brightness: brightness, + brightness: colorScheme.brightness, labelStyle: textTheme.bodyText1, ); dialogTheme ??= const DialogTheme(); @@ -382,7 +382,6 @@ class ThemeData with Diagnosticable { fixTextFieldOutlineLabel ??= false; return ThemeData.raw( - brightness: brightness, visualDensity: visualDensity, primaryColor: primaryColor, primaryColorBrightness: primaryColorBrightness, @@ -462,7 +461,6 @@ class ThemeData with Diagnosticable { // Warning: make sure these properties are in the exact same order as in // operator == and in the hashValues method and in the order of fields // in this class, and in the lerp() method. - @required this.brightness, @required this.visualDensity, @required this.primaryColor, @required this.primaryColorBrightness, @@ -528,8 +526,7 @@ class ThemeData with Diagnosticable { @required this.buttonBarTheme, @required this.bottomNavigationBarTheme, @required this.fixTextFieldOutlineLabel, - }) : assert(brightness != null), - assert(visualDensity != null), + }) : assert(visualDensity != null), assert(primaryColor != null), assert(primaryColorBrightness != null), assert(primaryColorLight != null), @@ -659,7 +656,7 @@ class ThemeData with Diagnosticable { /// this theme is localized using text geometry using [ThemeData.localize]. factory ThemeData.light() => ThemeData(brightness: Brightness.light); - /// A default dark theme with a teal accent color. + /// A default dark theme with a teal secondary [ColorScheme] color. /// /// This theme does not contain text geometry. Instead, it is expected that /// this theme is localized using text geometry using [ThemeData.localize]. @@ -680,17 +677,12 @@ class ThemeData with Diagnosticable { // hashValues() and in the raw constructor and in the order of fields in // the class and in the lerp() method. - /// The brightness of the overall theme of the application. Used by widgets - /// like buttons to determine what color to pick when not using the primary or - /// accent color. + /// The overall theme brightness. /// - /// When the [Brightness] is dark, the canvas, card, and primary colors are - /// all dark. When the [Brightness] is light, the canvas and card colors - /// are bright, and the primary color's darkness varies as described by - /// primaryColorBrightness. The primaryColor does not contrast well with the - /// card and canvas colors when the brightness is dark; when the brightness is - /// dark, use Colors.white or the accentColor for a contrasting color. - final Brightness brightness; + /// The default [TextStyle] color for the [textTheme] is black if the + /// theme is constructed with [Brightness.light] and white if the + /// theme is constructed with [Brightness.dark]. + Brightness get brightness => colorScheme.brightness; /// The density value for specifying the compactness of various UI components. /// @@ -1054,6 +1046,8 @@ class ThemeData with Diagnosticable { final bool fixTextFieldOutlineLabel; /// Creates a copy of this theme but with the given fields replaced with the new values. + /// + /// The [brightness] value is applied to the [colorScheme]. ThemeData copyWith({ Brightness brightness, VisualDensity visualDensity, @@ -1124,7 +1118,6 @@ class ThemeData with Diagnosticable { }) { cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault(); return ThemeData.raw( - brightness: brightness ?? this.brightness, visualDensity: visualDensity ?? this.visualDensity, primaryColor: primaryColor ?? this.primaryColor, primaryColorBrightness: primaryColorBrightness ?? this.primaryColorBrightness, @@ -1176,7 +1169,7 @@ class ThemeData with Diagnosticable { pageTransitionsTheme: pageTransitionsTheme ?? this.pageTransitionsTheme, appBarTheme: appBarTheme ?? this.appBarTheme, bottomAppBarTheme: bottomAppBarTheme ?? this.bottomAppBarTheme, - colorScheme: colorScheme ?? this.colorScheme, + colorScheme: (colorScheme ?? this.colorScheme).copyWith(brightness: brightness), dialogTheme: dialogTheme ?? this.dialogTheme, floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme, navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme, @@ -1271,7 +1264,6 @@ class ThemeData with Diagnosticable { // hashValues() and in the raw constructor and in the order of fields in // the class and in the lerp() method. return ThemeData.raw( - brightness: t < 0.5 ? a.brightness : b.brightness, visualDensity: VisualDensity.lerp(a.visualDensity, b.visualDensity, t), primaryColor: Color.lerp(a.primaryColor, b.primaryColor, t), primaryColorBrightness: t < 0.5 ? a.primaryColorBrightness : b.primaryColorBrightness, @@ -1348,7 +1340,6 @@ class ThemeData with Diagnosticable { // hashValues() and in the raw constructor and in the order of fields in // the class and in the lerp() method. return other is ThemeData - && other.brightness == brightness && other.visualDensity == visualDensity && other.primaryColor == primaryColor && other.primaryColorBrightness == primaryColorBrightness @@ -1420,7 +1411,6 @@ class ThemeData with Diagnosticable { // are in the exact same order as in operator == and in the raw constructor // and in the order of fields in the class and in the lerp() method. final List values = [ - brightness, visualDensity, primaryColor, primaryColorBrightness, diff --git a/packages/flutter/test/material/theme_data_test.dart b/packages/flutter/test/material/theme_data_test.dart index 862fda8a08..986d956b79 100644 --- a/packages/flutter/test/material/theme_data_test.dart +++ b/packages/flutter/test/material/theme_data_test.dart @@ -216,7 +216,6 @@ void main() { ); final ThemeData theme = ThemeData.raw( - brightness: Brightness.dark, visualDensity: const VisualDensity(), primaryColor: Colors.black, primaryColorBrightness: Brightness.dark, @@ -298,7 +297,6 @@ void main() { ); final ThemeData otherTheme = ThemeData.raw( - brightness: Brightness.light, visualDensity: const VisualDensity(), primaryColor: Colors.white, primaryColorBrightness: Brightness.light, @@ -367,7 +365,6 @@ void main() { ); final ThemeData themeDataCopy = theme.copyWith( - brightness: otherTheme.brightness, primaryColor: otherTheme.primaryColor, primaryColorBrightness: otherTheme.primaryColorBrightness, primaryColorLight: otherTheme.primaryColorLight, @@ -517,4 +514,27 @@ void main() { expect(lightTheme.toString().length, lessThan(200)); }); + + testWidgets('ThemeData brightness parameter overrides ColorScheme brightness', (WidgetTester tester) async { + const ColorScheme lightColors = ColorScheme.light(); + expect(() => ThemeData(colorScheme: lightColors, brightness: Brightness.dark), throwsAssertionError); + }); + + testWidgets('ThemeData.copyWith brightness parameter overrides ColorScheme brightness', (WidgetTester tester) async { + const ColorScheme lightColors = ColorScheme.light(); + final ThemeData theme = ThemeData.from(colorScheme: lightColors).copyWith(brightness: Brightness.dark); + + // The brightness parameter only overrides ColorScheme.brightness. + expect(theme.brightness, equals(Brightness.dark)); + expect(theme.colorScheme.brightness, equals(Brightness.dark)); + expect(theme.primaryColor, equals(lightColors.primary)); + expect(theme.accentColor, equals(lightColors.secondary)); + expect(theme.cardColor, equals(lightColors.surface)); + expect(theme.backgroundColor, equals(lightColors.background)); + expect(theme.canvasColor, equals(lightColors.background)); + expect(theme.scaffoldBackgroundColor, equals(lightColors.background)); + expect(theme.dialogBackgroundColor, equals(lightColors.background)); + expect(theme.errorColor, equals(lightColors.error)); + expect(theme.applyElevationOverlayColor, isFalse); + }); }