Added MaterialApp.themeMode to control which theme is used. (#35499)

Added support for a themeMode property to the MaterialApp to control
how the light or dark theme is selected.
This commit is contained in:
Darren Austin 2019-07-09 15:26:42 -07:00 committed by GitHub
parent 42a01befa0
commit af1bd09c78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 178 additions and 16 deletions

View file

@ -34,6 +34,19 @@ const TextStyle _errorTextStyle = TextStyle(
debugLabel: 'fallback style; consider putting your text in a Material', 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. /// An application that uses material design.
/// ///
/// A convenience widget that wraps a number of widgets that are commonly /// A convenience widget that wraps a number of widgets that are commonly
@ -99,6 +112,7 @@ class MaterialApp extends StatefulWidget {
this.color, this.color,
this.theme, this.theme,
this.darkTheme, this.darkTheme,
this.themeMode = ThemeMode.system,
this.locale, this.locale,
this.localizationsDelegates, this.localizationsDelegates,
this.localeListResolutionCallback, this.localeListResolutionCallback,
@ -169,13 +183,15 @@ class MaterialApp extends StatefulWidget {
/// Default visual properties, like colors fonts and shapes, for this app's /// Default visual properties, like colors fonts and shapes, for this app's
/// material widgets. /// material widgets.
/// ///
/// A second [darkTheme] [ThemeData] value, which is used when the underlying /// A second [darkTheme] [ThemeData] value, which is used to provide a dark
/// platform requests a "dark mode" UI, can also be specified. /// 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()]. /// The default value of this property is the value of [ThemeData.light()].
/// ///
/// See also: /// See also:
/// ///
/// * [themeMode], which controls which theme to use.
/// * [MediaQueryData.platformBrightness], which indicates the platform's /// * [MediaQueryData.platformBrightness], which indicates the platform's
/// desired brightness and is used to automatically toggle between [theme] /// desired brightness and is used to automatically toggle between [theme]
/// and [darkTheme] in [MaterialApp]. /// and [darkTheme] in [MaterialApp].
@ -183,20 +199,21 @@ class MaterialApp extends StatefulWidget {
/// colors. /// colors.
final ThemeData theme; final ThemeData theme;
/// The [ThemeData] to use when the platform specifically requests a dark /// The [ThemeData] to use when a 'dark mode' is requested by the system.
/// themed UI.
/// ///
/// Host platforms such as Android Pie can request a system-wide "dark mode" /// Some host platforms allow the users to select a system-wide 'dark mode',
/// when entering battery saver 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.
/// ///
/// When the host platform requests a [Brightness.dark] mode, you may want to /// This theme should have a [ThemeData.brightness] set to [Brightness.dark].
/// supply a [ThemeData.brightness] that's also [Brightness.dark].
/// ///
/// Uses [theme] instead when null. Defaults to the value of /// Uses [theme] instead when null. Defaults to the value of
/// [ThemeData.light()] when both [darkTheme] and [theme] are null. /// [ThemeData.light()] when both [darkTheme] and [theme] are null.
/// ///
/// See also: /// See also:
/// ///
/// * [themeMode], which controls which theme to use.
/// * [MediaQueryData.platformBrightness], which indicates the platform's /// * [MediaQueryData.platformBrightness], which indicates the platform's
/// desired brightness and is used to automatically toggle between [theme] /// desired brightness and is used to automatically toggle between [theme]
/// and [darkTheme] in [MaterialApp]. /// and [darkTheme] in [MaterialApp].
@ -204,6 +221,32 @@ class MaterialApp extends StatefulWidget {
/// [MediaQueryData.platformBrightness]. /// [MediaQueryData.platformBrightness].
final ThemeData darkTheme; 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} /// {@macro flutter.widgets.widgetsApp.color}
final Color color; final Color color;
@ -454,15 +497,16 @@ class _MaterialAppState extends State<MaterialApp> {
onUnknownRoute: widget.onUnknownRoute, onUnknownRoute: widget.onUnknownRoute,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
// Use a light theme, dark theme, or fallback theme. // Use a light theme, dark theme, or fallback theme.
final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
ThemeData theme; ThemeData theme;
final ui.Brightness platformBrightness = MediaQuery.platformBrightnessOf(context); if (widget.darkTheme != null) {
if (platformBrightness == ui.Brightness.dark && widget.darkTheme != null) { final ui.Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);
theme = widget.darkTheme; if (mode == ThemeMode.dark ||
} else if (widget.theme != null) { (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark)) {
theme = widget.theme; theme = widget.darkTheme;
} else { }
theme = ThemeData.fallback();
} }
theme ??= widget.theme ?? ThemeData.fallback();
return AnimatedTheme( return AnimatedTheme(
data: theme, data: theme,

View file

@ -447,7 +447,99 @@ void main() {
expect(find.text('Select All'), findsOneWidget); expect(find.text('Select All'), findsOneWidget);
}); });
testWidgets('MaterialApp uses regular theme when platformBrightness is light', (WidgetTester tester) async { testWidgets('MaterialApp uses regular theme when themeMode is light', (WidgetTester tester) async {
// Mock the Window to explicitly report a light platformBrightness.
tester.binding.window.platformBrightnessTestValue = Brightness.light;
ThemeData appliedTheme;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
brightness: Brightness.light
),
darkTheme: ThemeData(
brightness: Brightness.dark,
),
themeMode: ThemeMode.light,
home: Builder(
builder: (BuildContext context) {
appliedTheme = Theme.of(context);
return const SizedBox();
},
),
),
);
expect(appliedTheme.brightness, Brightness.light);
// Mock the Window to explicitly report a dark platformBrightness.
tester.binding.window.platformBrightnessTestValue = Brightness.dark;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
brightness: Brightness.light
),
darkTheme: ThemeData(
brightness: Brightness.dark,
),
themeMode: ThemeMode.light,
home: Builder(
builder: (BuildContext context) {
appliedTheme = Theme.of(context);
return const SizedBox();
},
),
),
);
expect(appliedTheme.brightness, Brightness.light);
});
testWidgets('MaterialApp uses darkTheme when themeMode is dark', (WidgetTester tester) async {
// Mock the Window to explicitly report a light platformBrightness.
tester.binding.window.platformBrightnessTestValue = Brightness.light;
ThemeData appliedTheme;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
brightness: Brightness.light
),
darkTheme: ThemeData(
brightness: Brightness.dark,
),
themeMode: ThemeMode.dark,
home: Builder(
builder: (BuildContext context) {
appliedTheme = Theme.of(context);
return const SizedBox();
},
),
),
);
expect(appliedTheme.brightness, Brightness.dark);
// Mock the Window to explicitly report a dark platformBrightness.
tester.binding.window.platformBrightnessTestValue = Brightness.dark;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
brightness: Brightness.light
),
darkTheme: ThemeData(
brightness: Brightness.dark,
),
themeMode: ThemeMode.dark,
home: Builder(
builder: (BuildContext context) {
appliedTheme = Theme.of(context);
return const SizedBox();
},
),
),
);
expect(appliedTheme.brightness, Brightness.dark);
});
testWidgets('MaterialApp uses regular theme when themeMode is system and platformBrightness is light', (WidgetTester tester) async {
// Mock the Window to explicitly report a light platformBrightness. // Mock the Window to explicitly report a light platformBrightness.
final TestWidgetsFlutterBinding binding = tester.binding; final TestWidgetsFlutterBinding binding = tester.binding;
binding.window.platformBrightnessTestValue = Brightness.light; binding.window.platformBrightnessTestValue = Brightness.light;
@ -462,6 +554,7 @@ void main() {
darkTheme: ThemeData( darkTheme: ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
), ),
themeMode: ThemeMode.system,
home: Builder( home: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
appliedTheme = Theme.of(context); appliedTheme = Theme.of(context);
@ -474,6 +567,31 @@ void main() {
expect(appliedTheme.brightness, Brightness.light); expect(appliedTheme.brightness, Brightness.light);
}); });
testWidgets('MaterialApp uses darkTheme when themeMode is system and platformBrightness is dark', (WidgetTester tester) async {
// Mock the Window to explicitly report a dark platformBrightness.
tester.binding.window.platformBrightnessTestValue = Brightness.dark;
ThemeData appliedTheme;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
brightness: Brightness.light
),
darkTheme: ThemeData(
brightness: Brightness.dark,
),
themeMode: ThemeMode.system,
home: Builder(
builder: (BuildContext context) {
appliedTheme = Theme.of(context);
return const SizedBox();
},
),
),
);
expect(appliedTheme.brightness, Brightness.dark);
});
testWidgets('MaterialApp uses light theme when platformBrightness is dark but no dark theme is provided', (WidgetTester tester) async { testWidgets('MaterialApp uses light theme when platformBrightness is dark but no dark theme is provided', (WidgetTester tester) async {
// Mock the Window to explicitly report a dark platformBrightness. // Mock the Window to explicitly report a dark platformBrightness.
final TestWidgetsFlutterBinding binding = tester.binding; final TestWidgetsFlutterBinding binding = tester.binding;