Skip to content

Instantly share code, notes, and snippets.

@rydmike
Last active September 15, 2020 13:48
Show Gist options
  • Save rydmike/b59cea071eefdbe7c1332b2d014bc156 to your computer and use it in GitHub Desktop.
Save rydmike/b59cea071eefdbe7c1332b2d014bc156 to your computer and use it in GitHub Desktop.
Flutter: ColorScheme Theme Quality Issues
// MIT License
// Copyright 2020 Mike Rydstrom
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import 'package:flutter/material.dart';
const double kEdgePadding = 16.0;
const double kMaxContentWidth = 1200.0;
const ColorScheme lightScheme = ColorScheme.light();
const ColorScheme darkScheme = ColorScheme.dark();
final ThemeData lightTheme = ThemeData.from(colorScheme: lightScheme);
final ThemeData darkTheme = ThemeData.from(colorScheme: darkScheme);
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool isDarkMode;
bool useSelectionTheme;
@override
void initState() {
isDarkMode = false;
useSelectionTheme = true;
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Issue Discovery',
debugShowCheckedModeBanner: false,
theme: lightTheme.copyWith(useTextSelectionTheme: useSelectionTheme),
darkTheme: darkTheme.copyWith(useTextSelectionTheme: useSelectionTheme),
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
home: DemoPage(
title: 'Issue Discovery',
setDarkMode: (bool value) {
setState(() {
isDarkMode = value;
});
},
setSelectionTheme: (bool value) {
setState(() {
useSelectionTheme = value;
});
},
),
);
}
}
class DemoPage extends StatefulWidget {
const DemoPage({
Key key,
this.title,
this.setDarkMode,
this.setSelectionTheme,
}) : super(key: key);
final String title;
final ValueChanged<bool> setDarkMode;
final ValueChanged<bool> setSelectionTheme;
@override
_DemoPageState createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
bool isDarkMode;
bool useSelectionTheme;
@override
void initState() {
isDarkMode = false;
useSelectionTheme = true;
super.initState();
}
@override
Widget build(BuildContext context) {
final MediaQueryData media = MediaQuery.of(context);
final ThemeData theme = Theme.of(context);
final bool isDark = theme.brightness == Brightness.dark;
final double topPadding = media.padding.top;
final double bottomPadding = media.padding.bottom;
final TextTheme textTheme = theme.textTheme;
final TextStyle headline5 = textTheme.headline5;
final TextStyle headline4 = textTheme.headline4;
return Scaffold(
extendBodyBehindAppBar: true,
extendBody: true,
appBar: AppBar(
title: Text(widget.title),
elevation: 0,
),
body: Scrollbar(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kMaxContentWidth),
child: ListView(
padding: EdgeInsets.fromLTRB(
kEdgePadding,
topPadding + kToolbarHeight + kEdgePadding,
kEdgePadding,
kEdgePadding + bottomPadding,
),
children: <Widget>[
Text(
'ColorScheme Theme Quality Issues',
style: textTheme.headline4,
),
//
// Change theme
//
const Divider(),
Text('Completeness and Quality Issues', style: headline5),
const Text(
'ColorScheme themes create ThemeData that is inconsistent '
'with the provided ColorScheme. Some sub themes are also incomplete '
'when using ColorScheme based themes because ColorScheme does '
'not set all Theme() color properties used by sub themes.\n\n'
''
'For broader adoption of ColorScheme based theming, it would be '
'useful if it behaved as might be expected and was more '
'backwards compatible.\n\n'
''
'This issue demo uses "ColorScheme.light()" and '
'"ColorScheme.dark()" and Theme.from(colorscheme) for each '
'of them to demonstrate incompleteness and inconsistencies in '
'the resulting light and dark themes.',
),
const Divider(),
const Text(
'To observe inconsistencies, you need to switch between '
'dark and light theme, as some are ok in light theme and others '
'in dark theme and wise versa.',
),
SwitchListTile.adaptive(
title: const Text('Use dark theme'),
subtitle:
const Text('OFF for light theme, ON for dark theme.'),
value: isDarkMode,
onChanged: (bool value) {
setState(() {
isDarkMode = value;
widget.setDarkMode(isDarkMode);
});
},
),
const Divider(),
Text('toggleableActiveColor', style: headline5),
const Text(
'ColorScheme based themes will get the default dark theme based '
'toggleable color and not the secondary color from the dark '
'ColorScheme. The resulting color may or may not fit with '
'the used colors in ColorScheme based theme. The default dark theme '
'color is close to ColorScheme.dark() so it works fairly well for '
'the default values, but not for general usage.\n',
),
Wrap(
children: <Widget>[
const Text('Result:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.toggleableActiveColor,
child: Text(
'toggleableActiveColor\n\n'
'${theme.toggleableActiveColor}',
style: TextStyle(
color: theme.colorScheme.onSecondary)),
),
),
if (isDark) const Text('FAIL Expected:'),
if (!isDark) const Text('OK Expected:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.colorScheme.secondary,
child: Text(
'colorScheme.secondary\n\n'
'${theme.colorScheme.secondary}',
style: TextStyle(
color: theme.colorScheme.onSecondary)),
),
),
],
),
const Text(
'\nProposed fix:\n'
'ThemeData.from() should also set \n'
'toggleableActiveColor: colorScheme.secondary \n'
'for better backwards compatibility and so that the dark mode '
'toggleableActiveColor also matches the ColorScheme based theme '
'in dark mode and not only light mode.\n',
),
const Divider(),
Text('primaryColorDark', style: headline5),
const Text(
'ColorScheme based themes do not set any color at all '
"for the Theme() color primaryColorDark. It only gets default value from "
'default Theme factory for both light and dark mode. The resulting color '
'may or may not fit with the used colors in ColorScheme based theme.\n',
),
Wrap(
children: <Widget>[
const Text('Result:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.primaryColorDark,
child: Text(
'primaryColorDark\n\n'
'${theme.primaryColorDark}',
style: TextStyle(
color: isDark
? theme.colorScheme.onSurface
: theme.colorScheme.onPrimary)),
),
),
if (!isDark) const Text('FAIL Expected\ndarker hue of:'),
if (isDark) const Text('OK Expected\ndarker hue of:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.primaryColor,
child: Text(
'primaryColor\n\n'
'${theme.primaryColor}',
style: TextStyle(
color: isDark
? theme.colorScheme.onSurface
: theme.colorScheme.onPrimary)),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.colorScheme.primary,
child: Text(
'colorScheme.primary\n\n'
'${theme.colorScheme.primary}',
style:
TextStyle(color: theme.colorScheme.onPrimary)),
),
),
if (isDark)
const Text(
'It is a matter of design/opinion if it should '
'be darker than primaryColor or colorScheme.primary'),
const Text(
'\nProposed fix:\n'
'For a better fit with legacy themes and to match '
'the colors in the ColorScheme the ThemeData.from() '
'should set primaryColorDark to a slightly darker hue based on '
'colorScheme.primary for light themes.\n',
),
],
),
const Divider(),
Text('primaryColorLight', style: headline5),
const Text(
'ColorScheme based themes do not set any color at all '
"for the Theme() color primaryColorDark. It only gets default value from "
'default Theme factory for both light and dark mode. The resulting color '
'may or may not fit with the used colors in ColorScheme based theme.\n',
),
Wrap(
children: <Widget>[
const Text('Result:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.primaryColorLight,
child: Text(
'primaryColorLight\n\n'
'${theme.primaryColorLight}',
style:
TextStyle(color: theme.colorScheme.onSurface)),
),
),
if (!isDark) const Text('FAIL Expected\nlighter hue of:'),
if (isDark) const Text('OK Expected\nlighter hue of:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.primaryColor,
child: Text(
'primaryColor\n\n'
'${theme.primaryColor}',
style: TextStyle(
color: isDark
? theme.colorScheme.onSurface
: theme.colorScheme.onPrimary)),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.colorScheme.primary,
child: Text(
'colorScheme.primary\n\n'
'${theme.colorScheme.primary}',
style:
TextStyle(color: theme.colorScheme.onPrimary)),
),
),
if (isDark)
const Text(
'It is a matter of design/opinion if it should '
'be lighter than primaryColor or colorScheme.primary',
),
const Text(
'\nProposed fix:\n'
'For a better fit with legacy themes and to match '
'the colors in the ColorScheme the ThemeData.from() '
'should set primaryColorLight to a lighter hue based on '
'colorScheme.primary for light themes.\n',
),
],
),
const Divider(),
Text('secondaryHeaderColor', style: headline5),
const Text(
'ColorScheme based themes do not set any color at all '
"for the Theme() color secondaryHeaderColor. It only gets default value from "
'default Theme factory for both light and dark mode. The resulting color '
'may or may not fit with the used colors in ColorScheme based theme.\n',
),
Wrap(
children: <Widget>[
const Text('Result:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.secondaryHeaderColor,
child: Text(
'secondaryHeaderColor\n\n'
'${theme.secondaryHeaderColor}',
style:
TextStyle(color: theme.colorScheme.onSurface)),
),
),
if (!isDark)
const Text('FAIL Expected\nmuch lighter hue of:'),
if (isDark) const Text('OK Expected\nlighter hue of:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.primaryColor,
child: Text(
'primaryColor\n\n'
'${theme.primaryColor}',
style: TextStyle(
color: isDark
? theme.colorScheme.onSurface
: theme.colorScheme.onPrimary)),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.colorScheme.primary,
child: Text(
'colorScheme.primary\n\n'
'${theme.colorScheme.primary}',
style:
TextStyle(color: theme.colorScheme.onPrimary)),
),
),
if (isDark)
const Text(
'It is a matter of design/opinion if it should '
'be lighter than primaryColor or colorScheme.primary',
),
const Text(
'\nProposed fix:\n'
'For a better fit with legacy themes and to match '
'the colors in the ColorScheme the ThemeData.from() '
'should set secondaryHeaderColor to a much lighter hue based on '
'colorScheme.primary for light themes.\n',
),
],
),
const Divider(),
const Text(
"Just a repeat of dark/light toggle so I don't have to scroll "
'all the way back up to toggle it.',
),
SwitchListTile.adaptive(
title: const Text('Use dark theme'),
subtitle:
const Text('OFF for light theme, ON for dark theme.'),
value: isDarkMode,
onChanged: (bool value) {
setState(() {
isDarkMode = value;
widget.setDarkMode(isDarkMode);
});
},
),
const Divider(),
Text('Text selection and ColorScheme', style: headline4),
const Text(
'The new "TextSelectionThemeData" and "useTextSelectionTheme" '
'partially address the issues below, but only when the colors are used in '
'"TextField() and "SelectableText()". Still the color properties in the '
'resulting ThemeData are left at their default values, which may be confusing and '
'they also do not match the applied ColorScheme based theme.',
),
SwitchListTile.adaptive(
title: const Text('Use text selection theme'),
subtitle: const Text(
'OFF for no selection theme, ON for text selection theme.'),
value: useSelectionTheme,
onChanged: (bool value) {
setState(() {
useSelectionTheme = value;
widget.setSelectionTheme(useSelectionTheme);
});
},
),
const Divider(),
const Text(
'Use the text field below to enter some text and select it '
'to see the difference between using it and not using the '
'text selection theme. Wether it is used or not, does not have '
'any impact on the ThemeData proprties below as defined by the ColorScheme.',
),
const TextField(),
const Text(
'\nThe "useTextSelectionTheme" cannot be set with ColorScheme, '
'an extra copyWith is required and even then the ThemeData result '
'values are left at their Theme() defaults, this may be confusing.',
),
const Divider(),
Text('textSelectionColor', style: headline5),
const Text(
'ColorScheme based themes do not set any color at all '
"for the Theme() color textSelectionColor. It only gets default value from "
'default Theme factory for both light and dark mode. The resulting color '
'may or may not fit with the used colors in ColorScheme based theme.\n',
),
Wrap(
children: <Widget>[
const Text('Result:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.textSelectionColor,
child: Text(
'textSelectionColor\n\n'
'${theme.cursorColor}',
style:
TextStyle(color: theme.colorScheme.onSurface)),
),
),
if (!isDark)
const Text(
'FAIL Expected\nlighter hue of\ncolorScheme.primary:'),
if (isDark)
const Text('FAIL Expected\ncolorScheme.secondary:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.colorScheme.primary,
child: Text(
'colorScheme.primary\n\n'
'${theme.colorScheme.primary}',
style:
TextStyle(color: theme.colorScheme.onPrimary)),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.colorScheme.secondary,
child: Text(
'colorScheme.secondary\n\n'
'${theme.colorScheme.secondary}',
style: TextStyle(
color: theme.colorScheme.onSecondary)),
),
),
if (!isDark)
const Text(
'If following textSelectionTheme, then expected colorScheme.primary with opacity(0.12)'),
if (isDark)
const Text(
'If following textSelectionTheme, then expected colorScheme.primary with opacity(0.40)'),
const Text(
'\nProposed fix:\n'
'For a better fit with legacy themes and to match '
'the colors in the ColorScheme the ThemeData.from() '
'should set textSelectionColor to a lighter hue based on '
'colorScheme.primary for light themes and to colorScheme.secondary '
'for dark themes.\n'
'Alternatively:\n'
'ColorScheme based themes could '
'set the Theme() property to the colorScheme.primary based '
'colors introduced with TextSelectionThemeData, this would not '
'break older Theme() based themes and would make the property '
'correctly ColorScheme based.\n',
),
],
),
const Divider(),
Text('textSelectionHandleColor', style: headline5),
const Text(
'ColorScheme based themes do not set any color at all '
"for the Theme() color textSelectionHandleColor. It only gets default value from "
'default Theme factory for both light and dark mode. The resulting color '
'may or may not fit with the used colors in ColorScheme based theme.\n',
),
Wrap(
children: <Widget>[
const Text('Result:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.textSelectionHandleColor,
child: Text(
'textSelectionHandleColor\n\n'
'${theme.textSelectionHandleColor}',
style:
TextStyle(color: theme.colorScheme.onSurface)),
),
),
if (!isDark)
const Text(
'FAIL Expected\nlighter hue of\ncolorScheme.primary:'),
if (isDark)
const Text('FAIL Expected\ncolorScheme.secondary:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.colorScheme.primary,
child: Text(
'colorScheme.primary\n\n'
'${theme.colorScheme.primary}',
style:
TextStyle(color: theme.colorScheme.onPrimary)),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.colorScheme.secondary,
child: Text(
'colorScheme.secondary\n\n'
'${theme.colorScheme.secondary}',
style: TextStyle(
color: theme.colorScheme.onSecondary)),
),
),
],
),
const Text(
'If following textSelectionTheme, then expected colorScheme.primary'),
const Text(
'\nProposed fix:\n'
'For a better fit with legacy themes and to match '
'the colors in the ColorScheme the ThemeData.from() '
'should set textSelectionHandleColor to a lighter hue based on '
'colorScheme.primary for light themes and to a darker hue of '
'colorScheme.secondary for dark themes.\n'
'Alternatively:\n'
'ColorScheme based themes could '
'set the Theme() property to the colorScheme.primary based '
'colors introduced with TextSelectionThemeData, this would not '
'break older Theme() based themes and would make the property '
'correctly ColorScheme based.\n',
),
const Divider(),
Text('cursorColor', style: headline5),
const Text(
'ColorScheme based themes do not set any color at all '
"for the Theme() color cursorColor. It only gets default value from "
'default Theme factory for both light and dark mode. The resulting color '
'may or may not fit with the used colors in ColorScheme based theme.\n',
),
Wrap(
children: <Widget>[
const Text('Result:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.cursorColor,
child: Text(
'cursorColor\n\n'
'${theme.cursorColor}',
style:
TextStyle(color: theme.colorScheme.onSurface)),
),
),
const Text(
'FAIL Expected\ncolorScheme.primary\nor a hue of it:'),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: theme.colorScheme.primary,
child: Text(
'colorScheme.primary\n\n'
'${theme.colorScheme.primary}',
style:
TextStyle(color: theme.colorScheme.onPrimary)),
),
),
],
),
const Text(
'If following textSelectionTheme, then expected colorScheme.primary'),
const Text(
'\nProposed fix:\n'
'For a better fit with legacy themes and to match '
'the colors in the ColorScheme the ThemeData.from() '
'should set cursorColor to colorScheme.primary or some hue of it.\n'
'Alternatively:\n'
'ColorScheme based themes could '
'set the Theme() property to the colorScheme.primary based '
'colors introduced with TextSelectionThemeData, this would not '
'break older Theme() based themes and would make the property '
'correctly ColorScheme based.\n\n'
'It is worth noticing that default Theme() uses the same fixed blue '
'color for light and dark mode and that this blue color '
'is not any variant of the default blue colors used in the default '
'blue primary swatch, which is a bit odd. It is very close to the '
'default blue hues in default Theme() and thus works well with the '
'the default Theme(), but it does not work well with ColorScheme '
'themes or any other none default and none blue based theme.\n',
),
const Divider(),
Text('Source code for this demo', style: headline5),
const SelectableText(
'https://gist.github.com/rydmike/b59cea071eefdbe7c1332b2d014bc156'),
const Divider(),
],
),
),
),
),
);
}
}
@rydmike
Copy link
Author

rydmike commented Sep 14, 2020

Demo code for Flutter issue:
ColorScheme Theme Quality Issues - Gaps in completeness and inconsistencies #65782
flutter/flutter#65782

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment