rydmike / flex_color_scheme

A Flutter package to make and use beautiful color scheme based themes.
Other
947 stars 105 forks source link

Provides copywith for FlexScheme #205

Closed laterdayi closed 11 months ago

laterdayi commented 11 months ago

I want to use the default theme FlexScheme.indigoM3..... But since I am now dynamic FlexScheme.****, I need to change one of the colors, which I can't seem to do right now

laterdayi commented 11 months ago

I can currently change the theme dynamically, and hopefully when I switch to FlexScheme.blue or FlexScheme.material, my success color will always be Colors.green

rydmike commented 11 months ago

First question - Built-in theme color overrides

There are different ways you can override the built-in FlexColorScheme scheme colors. The simplest way is to just give a color to a specific color in FlexColorScheme. We can use example 4 as a simple demo.

In example 4 "All themes" https://docs.flexcolorscheme.com/tutorial4 user can dynamically change app theme to any of the built-in schemes and a few custom made ones, try it here https://rydmike.com/flexcolorscheme/allthemes-latest/.

If we in this app want to override one of the colors in all the built-in color schemes, it is actually quite easy to do.

We can demo it by modifying the example to for all selected themes make e.g. the tertiary color scheme color a fixed green color in all selected schemes.

class DemoApp extends StatelessWidget {
  const DemoApp({super.key, required this.themeController});
  final ThemeController themeController;

  @override
  Widget build(BuildContext context) {
    // Whenever the theme controller notifies the listenable in the
    // ListenableBuilder, the MaterialApp is rebuilt.
    return ListenableBuilder(
      listenable: themeController,
      builder: (BuildContext context, Widget? child) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'All Themes',
          // Define the light theme for the app, using current scheme index.
          theme: FlexThemeData.light(
            // Make tertiary always green in all selected schemes.
            // Here I am using one of the built in FlexColor color consts,
            // but you can use any color value.
            tertiary: FlexColor.greenLightPrimary, // <================= modified line
            useMaterial3: themeController.useMaterial3,
            // We moved the definition of the list of color schemes to use into
            // a separate static class and list. We use the theme controller
            // to change the index of used color scheme from the list.
            colors: AppColor.customSchemes[themeController.schemeIndex].light,
            // Here we use another surface blend mode, where the scaffold
            // background gets a strong blend. This type is commonly used
            // on web/desktop when you wrap content on the scaffold in a
            // card that has a lighter background.
            surfaceMode: FlexSurfaceMode.highScaffoldLowSurfaces,

Do the same for dark mode, you need another shade of green for it to look nice, but same idea:

          // We do the exact same definition for the dark theme, but using
          // FlexThemeData.dark() and the dark FlexSchemeColors in our
          // AppColor.schemes list instead.
          darkTheme: FlexThemeData.dark(
            tertiary: FlexColor.greenDarkPrimary,  // <========= a green for dark mode
            useMaterial3: themeController.useMaterial3,
            colors: AppColor.customSchemes[themeController.schemeIndex].dark,
            surfaceMode: FlexSurfaceMode.highScaffoldLowSurfaces,
            // We go with a slightly stronger blend in dark mode.
            blendLevel: 7,

Why I used tertiary for this color is because by default in M2/M3 widget color mapping, the tertiary ColorScheme color is not used by any built-in Material widget by default, so they will not get affected by this change.

However, if you enable seeded ColorScheme, then this tertiary color may get overridden, but if you lock it down to seed value, it will still be used, but then the tertiaryContainer will also get a green tone derived from the tertiary color, and tertiaryContainer color is used by one widget by default, the TimePicker, so you will get a green color in it, unless you customize this widget theme to use another color scheme color for that particular usage.

Here you can see the green color in the selected dark red red wine theme in M3 mode: Screenshot 2023-10-19 at 12 27 38

Here in light Material2 default theme in M2: Screenshot 2023-10-19 at 12 29 52

It is now in every theme in your app regardless of what theme and mode you selected dynamically in the app.

You can the use the color via ´Theme.of(context).colorScheme.tertiary` in any widget.

However, THIS is probably not what you want to do, because is in your second part of your question you mention "success" color. It is also messy, since it messes with the ColorScheme colors that should be used by Material widgets. What if there was a way to add extra semantic colors to your theme? You can do that too! 💙 😄

Second question - Semantic colors

The Flutter Material theme deals with all the colors needed to give your app and all Material widgets used in itn a given style and cohesive set of colors.

The built-in Material ColorScheme is used to define these colors. These colors are not intended to be used for semantic colors. That means colors that are associated with a common meaning in your app. Yes, ColorScheme does contain one semantic color and that is the red error color and its variants, but it is the only one.

There is basically no built-in default placement for a lot of different semantic colors in the Material ColorScheme. This is because it is impossible to know what semantic colors your app will need.

Maybe just "success" is enough, but what about "warning", "waiting", or maybe you have a delivery app that deals with order status that you want to associate with given colors "received", "processing", "shipped", "delayed", "delivered". Or maybe energy forms that should be associated with fixed colors in charts for "electric energy", "district heat", "oil", "gas", "district cooling" etc... well you get the idea.

The Themes Playground app contains its own example on how handle its own semantic colors, it is in the code view. The code key word highlight colors are treated as semantic colors outside the widget and app theme colors, see here:

Screenshot 2023-10-19 at 12 43 11

However, in this case the colors are not fixed, they are dynamically tuned and harmonized to fit better with the selected primary color of the active theme, so they sometimes get a bit different color shade when the theme color changes. This is done so they fit a bit better with the overall theme, but they still stay true to their fixed source "color".

The base starting point colors for dark and light mode are of course different, for good contrast, above I was talking about them also being adjusted within the light or dark mode as its primary color changes.

Select different themes and see the color boxes and used code highlight colors being auto tuned to the surrounding theme.

The same need and idea is true for e.g. a green "Success" color, a green color that looks good in a reddish theme, might not do so in a bluish theme.

To just store a bunch of extra color values, typically semantic colors, that you can access via the Theme.of(context), you should use theme extensions, they are directly supported by FlexColorScheme too and you add them the same way as with vanilla ThemeData.

To harmonize the colors dynamically, e.g. towards primary color of the theme, you need to use the Material Color Utilities harmonize function on these color. FlexColorScheme also provides this color utility, so you do not need to import anything extra to use it. You can even build the harmonization into the theme extension so it happens automatically when the theme changes.

Semantic colors, theme extensions and color harmonization

Semantic colors are mentioned in M3 guide too https://m3.material.io/styles/color/dynamic-color/user-generated-color#65c3d69f-259f-49e9-91d1-1ae20be8ae73

And in detail here: https://m3.material.io/styles/color/the-color-system/custom-colors

Including the harmonization concept: https://m3.material.io/styles/color/the-color-system/custom-colors#0a23e6c1-3a6b-490d-a6b6-b7bce64314e2

To learn how to make theme extension this is the guide: https://api.flutter.dev/flutter/material/ThemeExtension-class.html the video is very good, watch it if you have not seen it.

In the FlexColorScheme docs the theme extension usage and support is briefly mentioned here https://api.flutter.dev/flutter/material/ThemeExtension-class.html

There are some examples around on M3 color harmonization in #Flutter but not many. You can just take a look in the Themes Playground code base on how the code highlight colors are made.

Another maybe simpler, or at least smaller code base, example can be found in material from a paid theming workshop that I held in Stockholm at Flutter'N'Friends Flutter event not so long ago.

The workshop intro slides are available, as is the workshop GitHub repo used during the workshop, the workshop was however not recorded. But all the tutorial/workshop material is available. In this workshop I showed and thought a lot of theming tricks without FlexColorScheme and then also show how to do it easier with FlexColorScheme and go even further by making platform adaptive themes with FlexColorScheme.

The setup of the starter theme includes a simple theme extension for semantic "order" status colors, and also harmonizing the fixed source colors towards the active theme's primary color.

Enough said you can find repo and steps here: https://github.com/rydmike/theming_workshop There is a branch for each step in the workshop.

The theme extension with color harmonization example was just given as part of the intro talk, about why it is useful and good. Demo setup code here https://github.com/rydmike/theming_workshop/blob/master/lib/workshop/theme/avo_theme_ext.dart

Here you can see how it is used in a platform adaptive FlexColorScheme theme https://github.com/rydmike/theming_workshop/blob/fad4dec4b53e5738d598d99a0351d6925f37910c/lib/workshop/theme/avo_theme.dart#L669

The usage of the made extension is exactly the same in ThemeData https://github.com/rydmike/theming_workshop/blob/fad4dec4b53e5738d598d99a0351d6925f37910c/lib/workshop/theme/avo_theme.dart#L221

This shows how to use the order status on home screen:

https://github.com/rydmike/theming_workshop/blob/fad4dec4b53e5738d598d99a0351d6925f37910c/lib/workshop/screens/home_screen.dart#L141

Then the widget used to show it:

https://github.com/rydmike/theming_workshop/blob/fad4dec4b53e5738d598d99a0351d6925f37910c/lib/workshop/order_status/order_status_widgets.dart#L88

It is a bit fancy example, since the color also depends on the "status" of the order via an extended enum, but eventually you get down to where it picks up the harmonized color via the extension here:

https://github.com/rydmike/theming_workshop/blob/fad4dec4b53e5738d598d99a0351d6925f37910c/lib/workshop/order_status/order_status_model.dart#L80

The hole thing is also designed so that if the theme extension has not been added to the theme, it uses the fixed color tokens instead. This idea is useful if you make packages where users have the option add a theme extension for your package, but if they don't they still get good defaults.

Why not just fixed const color tokens?

The workshop demo app demonstrates why using just const color tokens, works, but is less nice, since:

A) They do not animate when the theme transitions, even just light/dark, they swap instantly.

Adding them via a theme extension makes them do that automatically. Sure, there are ways to make this animations happen without using theme extensions too, but it basically boils down to the same principle already used by ThemeData, and implementing that yourself. Why do that when ThemeData can already do it for you.

B) They do not harmonize towards active theme primary color.

If you have just one primary color for light and dark mode, that is fine, as you can set the colors in your theme extension to a value that is already color harmonious with your theme, you do not need to harmonize them, they already are. However, if you have many user selectable color schemes for light and dark mode, harmonization makes the custom/semantic colors look nicer and fit better which each color set.

Google Material team came up with color harmonization algorithm to make fixed app semantic colors fit better with dynamically generated color schemes, extracted from e.g. wallpaper/photo or any scheme color selected as input primary and secondaries by users.

If you build the workshop theme demo app, from its done step (branch 31-done), and set the theme animations to slow in debug mode in your IDE, then you can see very well how just const token based colors for the order status do not animate with light/dark switch, they just swap between light/dark color, where as all other theme based colors in the app lerp animates their colors from the light to the dark colors. This does not look very good imo.

However, with theme extension based additional theme colors, they animate with everything else when you change between light/dark mode. Well at least if you used different color shades for light/dark mode, which you must with most colors for good contrast.

https://github.com/rydmike/flex_color_scheme/assets/39990307/3d8aed2c-7d3c-4a4c-992b-f03d84e536c8

If you toggle to the example red red wine theme for the Deli's wine bar, you can see how the semantic colors change also to better "fit" with the red theme, when you just change theme in light mode, whereas the token based values remains fixed. This part comes via theme primary color harmonization. That this subtle change also animate is again because of the used theme extension based colors:

https://github.com/rydmike/flex_color_scheme/assets/39990307/375b431f-96b2-48e6-aa74-f0d12a55e4a4


Hope this answers your questions and helps, and maybe even teaches you some new Flutter and Material theming tricks.


I'm going to convert this reply to a Q&A for Discussions as well.