2. Custom Theme

This example shows how you can define your own color schemes using FlexSchemeColor and FlexSchemeData, to create FlexColorScheme based application themes from them.

Only code highlights might be shown below. The complete code of this example can be found here.

Skeleton Template

In this example, and the ones after it, we use a ThemeService and ThemeController to manage our theme settings. This follows the example architecture you get when you create a Flutter template application architecture with:

 > flutter create -t skeleton my_flutter_app

You can find a good article on GSkinner site blogs, doing a deep dive into the Flutter skeleton template architecture here.

These examples do not use all parts of the skeleton architecture, only the theme service part and using the ListenableBuilder as a listenable builder.

ThemeServiceMem

This example uses a theme service with only memory storage and no persistence. In later examples, we will use locally persisting theme services.

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // The used memory only theme service.
  final ThemeService themeService = ThemeServiceMem();
  // Initialize the theme service.
  await themeService.init();
  // Create a ThemeController that uses the ThemeService.
  final ThemeController themeController = ThemeController(themeService);
  // Load preferred theme settings, while the app is loading, before MaterialApp
  // is created, this prevents a theme change when the app is first displayed.
  await themeController.loadAll();
  // Run the app and pass in the ThemeController. The app listens to the
  // ThemeController for changes.
  runApp(DemoApp(themeController: themeController));
}

Above we use the theme controller to change the themeMode and to toggle opting in and out of using FlexColorScheme's opinionated component sub-themes.

Custom Colors

To make a custom color scheme, we for simplicity define it as a local constant in this example. We make a FlexSchemeData object with a name, description and all required FlexSchemeColor colors defined for light and matching dark schemes.

const FlexSchemeData _myFlexScheme = FlexSchemeData(
  name: 'Midnight blue',
  description: 'Midnight blue theme, custom definition of all colors',
  light: FlexSchemeColor(
    primary: Color(0xFF00296B),
    primaryContainer: Color(0xFFA0C2ED),
    secondary: Color(0xFFD26900),
    secondaryContainer: Color(0xFFFFD270),
    tertiary: Color(0xFF5C5C95),
    tertiaryContainer: Color(0xFFC8DBF8),
  ),
  dark: FlexSchemeColor(
    primary: Color(0xFFB1CFF5),
    primaryContainer: Color(0xFF3873BA),
    secondary: Color(0xFFFFD270),
    secondaryContainer: Color(0xFFD26900),
    tertiary: Color(0xFFC9CBFC),
    tertiaryContainer: Color(0xFF535393),
  ),
);

We could also have stored the light and dark scheme only in their own FlexSchemeColor objects, and added them directly in their respective colors property in FlexThemeData.light and FlexThemeData.dark. However, we will also use this information on the HomePage for the theme switch widget and to display the scheme name and description. Putting them in a FlexSchemeData object that bundles the light and dark scheme color FlexSchemeColor, plus a name and description, is a convenient way to pass it along and re-use the information on the HomePage.

We use the FlexSchemeData instance _myFlexScheme instance light and dark properties, as the colors value for our FlexThemeData.light and FlexThemeData.dark, that we then assign to the MaterialApp light theme property theme and darkTheme property respectively.

The setup is similar to how we used one of the built-in predefined FlexSchemeData objects in example 1 via its enum selection property, but in this case we defined our own custom FlexSchemeData in _myFlexScheme and used the colors property in FlexSchemeData to tell it to use those colors instead of a built-in scheme.

Theme Setup

Based on the Flutter Skeleton template architecture we glue the ThemeController to the MaterialApp. The Flutter standard AnimatedBuilder Widget listens to the ThemeController for changes.

The Flutter AnimatedBuilder is a bit oddly named for this use case. Here it serves the purpose of functioning as a "ListenableBuilder", that rebuilds its child once, when its Listenable value, the animation changes. Which it does whenever our ThemeController calls notifyListeners, when we update any of its properties. We call notifyListeners in the ThemeController class when we have new updated data that requires the theme to update. The ThemeController values are typically changed by user interface widgets on the HomePage.

ListenableBuilder or AnimatedBuilder

Previously the skeleton architecture used the AnimatedBuilder, so did these tutorial apps. The usage of the AnimatedBuilder did not have anything to do with the fact that the theme change animates from current ThemeData and colors in it, to the new values and colors it changes to. This is a built-in feature in ThemeData and its inherited Theme in Flutter SDK. You can change the ThemeData with call-backs or other state management solutions too, and still get the nice theme change lerp animation from previous property values to new ones.

The AnimatedBuilder was a poor name when used here as a ListenableBuilder, but it did not use to exist in Flutter SDK with that name. Flutter 3.10 brought same functionality as the AnimatedBuilder, but with the ListenableBuilder name, making it easier to discover the feature. The ListenableBuilder was added via PR #116543.

The result of this ListenableBuilder setup is that whenever you update any theme settings managed by the ThemeController, the MaterialApp is rebuilt with the new ThemeData that depends on the ThemeController's property values.

The entire application tree is actually rebuilt when any value in the ThemeController trigger a change via a notifyListeners call. This is reasonably fine in this use case, since all properties we use in the ThemeController are of the nature that the entire application UI needs to be redrawn anyway when they change, since they modify our ThemeData for the entire application.

This approach works well for this particular use case, but may not work as well for some use cases as it is a bit suboptimal to always rebuild the entire subtree whenever anything bound to the same ChangeNotifier controller changes. You could of course use the same idea to add more/other controllers and in StatefulWidgets at right levels in the widget tree use e.g. otherController.addListener(() => setState((){})). Then you do not need an ListenableBuilder and you get a basic MVC like state management, where you bind a ChangeNotifier to the tree and trigger rebuilds in a simple way.

Theming Code

This was a lot of explanations, no need to worry about most it for your theme definition, as shown below, it is simple.

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

  @override
  Widget build(BuildContext context) {
    // Whenever the theme controller notifies the listener in the
    // ListenableBuilder, the MaterialApp is rebuilt.
    return ListenableBuilder(
        listenable: themeController,
        builder: (BuildContext context, Widget? child) {
          return MaterialApp(
            debugShowCheckedModeBanner: false,
            scrollBehavior: const AppScrollBehavior(),
            title: 'Custom Theme',
            theme: FlexThemeData.light(
              useMaterial3: themeController.useMaterial3,
              colors: _myFlexScheme.light,
              // Opt in/out on using FlexColorScheme sub-themes.
              subThemesData: themeController.useSubThemes
                  ? const FlexSubThemesData()
                  : null,
              appBarElevation: 0.5,
              visualDensity: VisualDensity.standard,
              fontFamily: GoogleFonts.notoSans().fontFamily,
              // We use the nicer Material-3 Typography in both M2 and M3 mode.
              typography: Typography.material2021(platform: defaultTargetPlatform),
            ),
            // Same setup for the dark theme, but using FlexThemeData.dark().
            darkTheme: FlexThemeData.dark(
              useMaterial3: themeController.useMaterial3,
              colors: _myFlexScheme.dark,
              subThemesData: themeController.useSubThemes
                  ? const FlexSubThemesData()
                  : null,
              appBarElevation: 1,
              visualDensity: VisualDensity.standard,
              fontFamily: GoogleFonts.notoSans().fontFamily,
              typography: Typography.material2021(platform: defaultTargetPlatform),
            ),
            themeMode: themeController.themeMode,
            home: HomePage(
              flexSchemeData: _myFlexScheme,
              // Pass in the theme controller to the home page.
              controller: themeController,
            ),
          );
        });
  }
}

Component Themes

In this example above, we also use a boolean theme controller setting used as toggle to decide if we opt in our out of using FlexColorScheme opinionated component sub-themes. When the opt-in is true via themeController.useSubThemes we pass in a default FlexSubThemesData() constructor, otherwise null, to not use the sub-themes at all. Later we will explore sub-theme options that we can configure with FlexSubThemesData().

  // Opt in/out on using FlexColorScheme sub-themes.
  subThemesData: themeController.useSubThemes
      ? const FlexSubThemesData()
      : null,

Google Fonts

We also added a custom font by assigning one to fontFamily from Google fonts by using GoogleFonts.notoSans().fontFamily. The Noto Sans font is a nice alternative to the default Roboto font.

For better and more fine controlled results over your custom TextTheme, prefer defining complete TextThemes, using a font and its different styles. You can then use more than one font for your text theme. Then assign the TextTheme to the textTheme and primaryTextTheme properties in FlexThemeData. Both options work just as you would use them with ThemeData.

The themeController is also passed to the HomePage where we use it in UI widgets to change the theme mode, and to opt in and out of using the opinionated component theming features also offered by FlexColorScheme.

Result

When we build this example, we can see our custom colors being used on all widgets. We also notice that the widget components look a lot different compared to example 1. This is because we enabled using the FlexColorScheme opinionated component sub-themes by default when we used the themeController.useSubThemes value.

Custom1 light
 
Custom2 light
 
Custom3 light
Using custom colors with FlexColorScheme component themes, in a M2 mode light theme

In dark theme mode, it looks like this:

Custom1 dark
 
Custom2 dark
 
Custom3 dark
Using custom colors with FlexColorScheme component themes, in a M2 mode dark theme

We can compare the difference between using the FlexColorScheme opinionated component sub-themes and just default widget theming by toggling the switch for it.

The settings for opinionated component themes in Material-2 mode, use rounded corners on widgets that default to the Material-3 design guide specified values. In Material-3 design, the border radius varies per widget type. In the Flutter's current Material-2 design defaults, it is 4 dp on almost all widgets, this per the Material-2 design specification. Later we will see how we can easily customize the border radius on all components, as well as by component.

Custom4
 
Custom6
 
Custom5
 
Custom7
Custom colors M2 theme with component sub-themes enabled

If we keep Material-2 mode OFF and also turn OFF using the FlexColorScheme Material-2 mode component theming, we can see that the Flutter Material-2 defaults look very different. The FlexColorScheme Material-2 mode component themes are on purposed very opinionated, they draw inspiration from Material-3, but made in Material-2 mode, as far as that is possible in Flutter.

Custom5
 
Custom6
 
Custom5
 
Custom7
Custom colors M2 theme with component sub-themes disabled

Scroll down to see the theme showcase further below. It presents the theme with common Material UI widgets. You can try this example as a Flutter web app here.