Extending the Flutter Theme.

Stephan E.G. Veenstra
Pinch.nl
Published in
7 min readJan 8, 2022
Photo by Alice Dietrich on Unsplash

In Flutter’s ThemeData you’ll find all the things you need to style you app based on the material design guidelines.
There are color schemes, text themes and even styling for material components like the Floating Action Button.

But what if you want to define your own styling, for your custom Widget for example?
If you, just like me, have tried to do this, you most likely found out that this is not that easy to do.

But I found a way to do this which I would like to share with you!

Solution goals

The solution that I wanted to use should have the following three properties:

A. One theme to rule them all.
The goal is to have just one ‘theme’ object so you won’t end up with themes that get out of sync.

B. Theme should be context based
I want to have the theme come from context so I can ‘scope’ the theme to certain parts of the app as I please. This also means that when the context changes, I can adapt to it.

C. Our solution should be backwards compatible with the old theme.
We don’t want the default behavior to break and mess with the material components Flutter provides.

Solutions used by the community

I’ve looked and asked around on the internet to see which solutions the community is using. Two solutions came up the most:

Using extension methods.

I think this is the one I saw the most. We could use extension methods to add properties to ThemeData. The nice part is, that we don’t have to change the way we access the Theme. We can still use the normal Theme.of(context).
But, the drawback is that these properties are NOT instance properties which means that it’s not as dynamic.
This violates goal B.

extension ExtendedThemeData on ThemeData {
Color get myCustomColor => Colors.yellow;
}

Create another Theme-like object.

We could also just create our own separate object which would hold our custom properties. We could use an InheritedWidget (or a package like provider/riverpod) to access it from the context.
But now we have to keep track, and if needed, switch between multiple theme related objects, which violates goal A.

class CustomTheme {
final Color myCustomColor;
const CustomTheme({required this.myCustomColor});
}

Using extension methods was just not going to cut it. The only way this could become more dynamic would mean it should probably need some logic per property and I want the theme data to be as static as possible.

However, using a separate object does allow us to be dynamic. All I had to do was find a way to keep our custom theme in sync with the default theme.

My solution

So I liked the approach of creating a separate object, but hated the idea of having to manage multiple types of theme objects.
Luckily we can fix it, and all we have to do is create three simple classes…

MyThemeData

We start off by defining our own ThemeData class. You can name it whatever you like. It could be something generic, like AppThemeData, or something very app specific like WeatherAppThemeData. I recommend to end the name with ‘Data’. For this example I will call this class MyThemeData.

This class will hold all of your custom properties, like colors, spacings, or styling for complete widgets. One important thing is that it also includes a field to hold the default ThemeData. Notice that the themeData field is late and NOT final. This makes it easier to swap out later on to get the right behavior when accessing the default ThemeData.
Alternatively you could create a copyWith method (or use the copy_with_annotation package).

class MyThemeData {
late ThemeData themeData; // important!
final Color myCustomColor;
final CustomWidgetData customWidget;
MyThemeData({
required this.themeData,
required this.myCustomColor,
required this.customWidget,
});
}
class CustomWidgetData {
final Color backgroundColor;
final BoxShape shape;
const CustomWidgetData({
required this.backgroundColor,
required this.shape,
});

That’s it, just add all the properties you need.

InheritedMyTheme

We need a way to provide our newMyThemeData objects. We are going to use an InheritedWidget for this.

Because I named my ThemeData object MyThemeData, I will name this widget InheritedMyTheme. Again, it’s just a very simple, but powerful, class.
All it has to do is provide the MyThemeData that we want the underlying branches of the widget tree to use.

class InheritedMyTheme extends InheritedWidget {
final MyThemeData data;
const InheritedMyTheme({
required this.data,
required Widget child,
Key? key,
}):super(key: key, child: child,);
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) =>
oldWidget != this;
}

MyTheme

Finally, we are going to create the widget that we need to actually use our custom theme in our app.

It will be a StatelessWidget that we can put anywhere in the widget tree where we want to use our new custom theme. We will support both light and dark mode based on the OS configuration.

Another very important task of this widget is providing a normal Theme widget so it remains compatible, and so both theme objects stay in sync.

We will also add a convenient method so we can simple access our theme with MyTheme.of(context). You’ll notice that we are getting the themeData as well.
This is needed to make sure that Theme.of(context) and MyTheme.of(context).themeData will return the same values.

class MyTheme extends StatelessWidget {
final MyThemeData light;
final MyThemeData dark;
final Widget child;
const MyTheme({
required this.light,
required this.dark,
required this.child,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final brightness = MediaQuery.of(context).platformBrightness;
final data = brightness == Brightness.light ? light : dark;

return InheritedMyTheme(
data: data,
child: Theme(
data: data.themeData,
child: child,
),
);
}
static MyThemeData of(BuildContext context){
final theme = Theme.of(context);
return context
.dependOnInheritedWidgetOfExactType<InheritedMyTheme>()!
.data..themeData = theme;
}
}

This completes the solution. Let’s use it!

Usage

We’ve created multiple classes. Let’s see how we use them!

Create the themes

First we will create a light and a dark theme. To keep it simple I will just use ThemeData.light() and ThemeData.dark(). But since these are just the normal ThemeData objects you can configure them however you want.

final myLightTheme = MyThemeData(
themeData: ThemeData.light(),
myCustomColor: Colors.lightBlue,
customWidget: CustomWidgetData(
backgroundColor: Colors.lightGreen,
shape: BoxShape.circle,
),
);
final myDarkTheme = MyThemeData(
themeData: ThemeData.dark(),
myCustomColor: Colors.blue,
customWidget: CustomWidgetData(
backgroundColor: Colors.green,
shape: BoxShape.circle,
),
);

Using the themes

Usually you would provide your ‘main’ theme in the theme and darkTheme fields of theMaterialApp. But since our new theme is not a subtype of ThemeData, we can’t do that here.

We will use the builder field of MaterialApp which will allow us to put our MyTheme widget just above the Navigator, which is exactly what we want because then our theme gets used by all the pages we navigate to.

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
home: const Content(),
builder: (context, child) => MyTheme(
light: myLightTheme,
dark: myDarkTheme,
child: child ?? ErrorWidget('Child needed!'),
),
);
}
}

That’s all! We can now access our theme anywhere in the app, simply by calling MyTheme.of(context).

Remember the CustomWidgetData form earlier?
Let’s see how our CustomWidget could use our theme!

class CustomWidget extends StatelessWidget {

@override
Widget build(BuildContext context) {
final myTheme = MyTheme.of(context);
return Container(
decoration: BoxDecoration(
color: myTheme.customWidget.backgroundColor,
shape: myTheme.customWidget.shape,
),
child: Text(
'I use the default ThemeData',
style: myTheme.themeData.textTheme.headline1,
),
);
}
}

And that’s a wrap! We’re done.

Example

You can checkout this basic example I made on dartpad.

Conclusion

We’ve created our own solution to extend the theming in Flutter.
For this we’ve created a custom ThemeData class, an InheritedWidget and a StatelessWidget that will help us pass around our custom theme through context to stay dynamic.

By wrapping and providing the existing ThemeData we are able to keep both new and old themes in sync, which makes switching themes easy and concise, but also makes it compatible with the existing Theme.

Extra

Even though the amount of code we’ve written isn’t that much, it might get a bit annoying to write it for every project. There are also a few things that could be improved upon, but would require even more boilerplate code.

Therefor I started working on a package that I plan to release soon that would hopefully make things a bit easier.
So stay tuned!

Update (8 February 2022)

I’ve create a package called ext_theme on pub.dev that implements my solution. It will generate all the necessary classes for you based on some simple configuration.
I haven’t spend much time yet cleaning it up, but it does work.
I’m already using it in my own project.

Separate light and dark theme for Tiles

--

--

Stephan E.G. Veenstra
Pinch.nl

When Stephan learned about Flutter back in 2018, he knew this was his Future<>. In 2021 he quit his job and became a full-time Flutter dev.