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 for the 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 AnimatedBuilder
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 scheme.
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 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 kind of 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. Which we do 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
.
AnimatedBuilder
The usage of the AnimatedBuilder
does not have anything to do with the
fact that the theme changes animate 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
is a poor name when it is used here as a
ListenableBuilder
, that does not exist in Flutter SDK with that name. It
should, just for a better and more logical name, but the AnimatedBuilder
serves the exact same purpose here as a ListenableBuilder
would.
The result of this 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 sub-optimal to always rebuild the entire
sub-tree whenever anything bound to the same ChangeNotifier
controller changes. You could of course use the same idea to add more/other
controllers and in StatefulWidget
s at right levels in the widget tree use e.g.
otherController.addListener(() => setState((){}))
. Then you do not need
an AnimatedBuilder
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 very 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 animation listener in the
// AnimatedBuilder, the MaterialApp is rebuilt.
return AnimatedBuilder(
animation: themeController,
builder: (BuildContext context, Widget? child) {
return MaterialApp(
debugShowCheckedModeBanner: false,
scrollBehavior: const AppScrollBehavior(),
title: 'Custom Theme',
theme: FlexThemeData.light(
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,
),
// Same setup for the dark theme, but using FlexThemeData.dark().
darkTheme: FlexThemeData.dark(
colors: _myFlexScheme.dark,
subThemesData: themeController.useSubThemes
? const FlexSubThemesData()
: null,
appBarElevation: 1,
visualDensity: VisualDensity.standard,
fontFamily: GoogleFonts.notoSans().fontFamily,
),
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 the 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 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.
![]() |
![]() |
![]() | ||
Using a custom colors with FlexColorScheme as application light theme |
In dark theme mode it looks like this:
![]() |
![]() |
![]() | ||
Using a custom colors with FlexColorScheme as application 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 default settings for opinionated component themes 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 se how we can easily customize the border radius on all components, as well as by component.
![]() |
![]() |
![]() |
![]() | |||
Custom theme with component sub-themes enabled | ||||||
![]() |
![]() |
![]() |
![]() | |||
Custom theme with component sub-themes disabled |
Scroll down in the app 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.