'Is it possible "extend" ThemeData in Flutter

I might very well be missing something as I'm so new to flutter, but I'm finding ThemeData's options very limited (at least with my understanding of how to implement it).

If you look at this random design below from MaterialUp, I'd want to model something roughly like:

Themedata.cyclingColor = Color.pink; ThemeData.runningColor = Color.green;

That way everywhere in my app I can reference cycling, running, swimming, gym colors (Or whatever colors make sense in the context of my app/design) and keep things consistent.

Random design from MaterialUp

Is there a recommended way to achieve this currently in Flutter? What are my options?



Solution 1:[1]

I recommend this approach, which is simple, works with hot reload and can be easily extended to support switching between dark and light themes.

First create your own analog to ThemeData, let's call it AppThemeData:

class AppThemeData {
  final BorderRadius borderRadius = BorderRadius.circular(8);

  final Color colorYellow = Color(0xffffff00);
  final Color colorPrimary = Color(0xffabcdef);

  ThemeData get materialTheme {
    return ThemeData(
        primaryColor: colorPrimary
    );
  }
}

The materialTheme can be used whenever the standard ThemeData is needed.

Then create a widget called AppTheme, which provides an instance of AppThemeData using the provider package.

class AppTheme extends StatelessWidget {
  final Widget child;

  AppTheme({this.child});

  @override
  Widget build(BuildContext context) {
    final themeData = AppThemeData(context);
    return Provider.value(value: themeData, child: child);
  }
}

Finally, wrap the whole app with AppTheme. To access the theme you can call context.watch<AppThemeData>(). Or create this extension...

extension BuildContextExtension on BuildContext {
  AppThemeData get appTheme {
    return watch<AppThemeData>();
  }
}

... and use context.appTheme. I usually put final theme = context.appTheme; on the first line of the widget build method.

Solution 2:[2]

Updated for null-safety

I've extended standard ThemeData class so that at any time one could access own theme fields like that:

Theme.of(context).own().errorShade

Or like that:

ownTheme(context).errorShade

A theme can be defined and extended with new fields as follows(via addOwn() called on a certain ThemeData instance):

final ThemeData lightTheme = ThemeData.light().copyWith(
    accentColor: Colors.grey.withAlpha(128),
    backgroundColor: Color.fromARGB(255, 255, 255, 255),
    textTheme: TextTheme(
      caption: TextStyle(
          fontSize: 17.0, fontFamily: 'Montserrat', color: Colors.black),
    ))
  ..addOwn(OwnThemeFields(
      errorShade: Color.fromARGB(240, 255, 200, 200),
      textBaloon: Color.fromARGB(240, 255, 200, 200)));

final ThemeData darkTheme = ThemeData.dark().copyWith( ...
...

Themes can be applied to MaterialApp widget in a conventional way:

MaterialApp(
...
  theme: lightTheme,
  darkTheme: darkTheme,
)

The idea is to put all custom fields required for theming in a separate class OwnThemeFields.

Then extend ThemeData class with 2 methods:

  1. addOwn() that connects a certain instance of ThemedData to OwnThemeFields instance
  2. own() that allows to lookup for own fields associated with the given theme data

Also ownTheme helper method can be created to shorten the extraction of own fields.

class OwnThemeFields {
  final Color? errorShade;
  final Color? textBaloon;

  const OwnThemeFields({Color? errorShade, Color? textBaloon})
      : this.errorShade = errorShade,
        this.textBaloon = textBaloon;

  factory OwnThemeFields.empty() {
    return OwnThemeFields(errorShade: Colors.black, textBaloon: Colors.black);
  }
}
    
extension ThemeDataExtensions on ThemeData {
  static Map<InputDecorationTheme, OwnThemeFields> _own = {};

  void addOwn(OwnThemeFields own) {
    _own[this.inputDecorationTheme] = own;
  }

  static OwnThemeFields? empty = null;

  OwnThemeFields own() {
    var o = _own[this.inputDecorationTheme];
    if (o == null) {
      if (empty == null) empty = OwnThemeFields.empty();
      o = empty;
    }
    return o!;
  }
}

OwnThemeFields ownTheme(BuildContext context) => Theme.of(context).own();

Complete source: https://github.com/maxim-saplin/dikt/blob/master/lib/ui/themes.dart

Solution 3:[3]

You can't extend ThemeData because then material components won't find it anymore.

You can just create and provide MyThemeData in addition to the ThemeData included in Flutter the same way.

Create a widget CustomThemeWidget that extends InheritedWidget and provide your custom theme there.

When you want to get a value from the current theme use

myTheme = CustomThemeWidget.of(context).myTheme;

To change the current theme change the MyThemeData in CustomThemeWidget.myTheme

Update

Like shown in https://github.com/flutter/flutter/pull/14793/files, it should be possible to extend ThemeData and provide it as ThemeData by overriding runtimeType

See also the comment in https://github.com/flutter/flutter/issues/16487#event-1573761656

Solution 4:[4]

Dart 2.7 later, extension support

you can add extension for system class

only add instance property is easy, but if you would get a dynamic color

you need think about it. for example, Use a constant to get the colors in light and dark modes

Determine if it is dark mode

two ways

  • MediaQuery.of(context).platformBrightnes == Brightness.dark;
  • Theme.of(context).brightness == Brightness.dark;

As you can see, you need the context, the context

Add Extension for BuildContext

Here is the code

extension MYContext on BuildContext {
  Color dynamicColor({int light, int dark}) {
    return (Theme.of(this).brightness == Brightness.light)
        ? Color(light)
        : Color(dark);
  }

  Color dynamicColour({Color light, Color dark}) {
    return (Theme.of(this).brightness == Brightness.light)
        ? light
        : dark;
  }

  /// the white background
  Color get bgWhite => dynamicColor(light: 0xFFFFFFFF, dark: 0xFF000000);
}

How to use

import 'package:flutter/material.dart';
import 'buildcontext_extension.dart';

class Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: context.bgWhite,
    );
  }
}

Also

This color may require multiple files, so you can create a public.dart file to manage it all

Like This

public.dart


library public;

// Export some common header files

// extensions
export 'buildcontext_extension.dart';

DarkMode images support

Put the light images in the same category as the dark ones

some code

static String getImgPath(String name, {
    String folder = '', 
    String format = 'png', 
    bool isDark = false, 
    bool needDark = true
  }) {
    String finalImagePath;
    if (needDark) {
      final folderName = isDark ? '${folder}_dark' : folder;
      finalImagePath = 'assets/images/$folderName/$name.$format';
    } else {
      finalImagePath = 'assets/images/$folder/$name.$format';
    }
    String isDarkPath = isDark ? "? DarkMode" : "? LightMode";
    print('$isDarkPath imagePath ? $finalImagePath');
    return finalImagePath;
  }

Solution 5:[5]

I have also found that the ThemeData is restricting. What I have done, and will be doing for all of my apps in the future is creating my own ThemeData.

I have created a file named color_themes.dart and created a class named ColorThemes with constructors with the name of the colors that I desire. such as cyclingColor;

class ColorThemes {
    const static cyclingColor = const Color(0xffb74093); 
}

You can then call these colors by importing the file and calling ColorThemes.cyclingColor You can assign these values within your ThemeData to have these colors default to your ColorThemes. One of the benefits with using this method is that you do not need to use/reference context like so ThemeData.of(context) making it a lot easier to use your code in extracted widgets.

Solution 6:[6]

use this lib adaptive_theme for theme switch. And create extension of ColorSheme

extension MenuColorScheme on ColorScheme {
      Color get menuBackground => brightness == Brightness.light
          ? InlLightColors.White
          : InlDarkColors.Black;
}

In widget use that

Container(
      color: Theme.of(context).colorScheme.menuBackground,
...
)

This way is very simple and elegance. Nice to codding.

Solution 7:[7]

I solved this problem also for multiple themes by creating a CustomThemeData class like this:

class CustomThemeData {
    final double imageSize;

    CustomThemeData({
        this.imageSize = 100,
    });
}

Then, creating instances for each Theme:

final _customTheme = CustomThemeData(imageSize: 150);
final _customDarkTheme = CustomThemeData();

And writing an extension on ThemeData:

extension CustomTheme on ThemeData {
    CustomThemeData get custom => brightness == Brightness.dark ? _customDarkTheme : _customTheme;
}

Finally, the value can be accessed like this:

Theme.of(context).custom.imageSize

For more information see: https://bettercoding.dev/flutter/tutorial-themes-riverpod/#custom_attributes_extending_themedata

Solution 8:[8]

A simple workaround if you are not using all the textTheme headlines you can set some colors of some of them and use them like you normally use other colors.

set the headline1 color: ThemeData(textTheme: TextTheme(headline1: TextStyle(color: Colors.red),),),

Use it: RawMaterialButton(fillColor: Theme.of(context).textTheme.headline1.color,onPressed: onPressed,)

Solution 9:[9]

I created an implementation analog to the implementation of ThemeData:

@override
Widget build(BuildContext context) {
     final Brightness platformBrightness = Theme.of(context).brightness;
     final bool darkTheme = platformBrightness == Brightness.dark;

     return CustomAppTheme(
               customAppTheme:
                   darkTheme ? CustomAppThemeData.dark : CustomAppThemeData.light,
               child: Icon(Icons.add, color: CustomAppTheme.of(context).addColor,),
     );
}
import 'package:calendarflutter/style/custom_app_theme_data.dart';
import 'package:flutter/material.dart';

class CustomAppTheme extends InheritedWidget {
  CustomAppTheme({
    Key key,
    @required Widget child,
    this.customAppTheme,
  }) : super(key: key, child: child);

  final CustomAppThemeData customAppTheme;

  static CustomAppThemeData of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<CustomAppTheme>()
        .customAppTheme;
  }

  @override
  bool updateShouldNotify(CustomAppTheme oldWidget) =>
      customAppTheme != oldWidget.customAppTheme;
}
import 'package:flutter/material.dart';

class CustomAppThemeData {
  final Color plusColor;

  const CustomAppThemeData({
    @required this.plusColor,
  });

  static CustomAppThemeData get dark {
    return CustomAppThemeData(
      plusColor: Colors.red,
    );
  }

  static CustomAppThemeData get light {
    return CustomAppThemeData(
      plusColor: Colors.green,
    );
  }
}

Solution 10:[10]

To extend (pun not intended) the answer of Maxim Saplin:

You may encounter a problem, where theme stays on the last one initialized in your code. This is happening because InputDecorationTheme is always the same for all of yours themes.

What solved it for me, was changing key (InputDecorationTheme) in _own to something unique, like themeID (you'll have to implement it somehow).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3
Solution 4 CopsOnRoad
Solution 5 mrgnhnt96
Solution 6 ????????? ?????????? ??????
Solution 7 Stefan Galler
Solution 8
Solution 9 i4guar
Solution 10 Limtis