Welcome to Theme Tailor, a code generator and theming utility for supercharging Flutter ThemeExtension classes introduced in Flutter 3.0! The generator helps to minimize the required boilerplate code.
- Motivation
- How to use
- Install
- Add imports and part directive
- Run the code generator
- Create Theme class
- Change generated extensions
- Nesting generated ThemeExtensions, Modular themes && DesignSystems
- Custom types encoding
- Flutter diagnosticable / debugFillProperties
- Json serialization
- Build configuration
- Custom theme getter
- Migration from Tailor to TailorMixin
Flutter 3.0 introduces a new way of theming applications using theme extensions in ThemeData. To declare a theme extension, you need to create a class that extends ThemeData, define its constructor and fields, implement the "copyWith" and "lerp" methods, and optionally override the "hashCode," "==" operator, and implement the "debugFillProperties" method. Additionally you may want to create extensions on BuildContext or ThemeData to access newly created themes.
All of that involves extra coding work that is time-consuming and error-prone, which is why it is advisable to use a generator.
No code generation | @TailorMixin |
---|---|
The @TailorMixin
annotation generates a mixin with an implementation of the ThemeExtension class. It adopts a syntax familiar to standard ThemeExtension classes, allowing for enhanced customization of the resulting class.
It's worth noting that choosing either the @Tailor
or @TailorMixin
generator doesn't restrict you from using the other in the future.
In fact, the two generators can be used together to provide even more flexibility in managing your themes. Ultimately, both generators offer strong solutions for managing themes and can be used interchangeably to provide the level of customization that best suits your project.
ThemeTailor is a code generator and requires build_runner to run. Make sure to add these packages to the project dependencies:
- build_runner tool to run code generators (dev dependency)
- theme_tailor this package - theming utility (dev dependency)
- theme_tailor_annotation annotations for theme_tailor
flutter pub add --dev build_runner
flutter pub add --dev theme_tailor
flutter pub add theme_tailor_annotation
ThemeTailor is a generator for annotation that generates code in a part file that needs to be specified. Make sure to add the following imports and part directive in the file where you use the annotation.
Make sure to specify the correct file name in a part directive. In the example below, replace "name" with the file name.
import 'package:theme_tailor_annotation/theme_tailor_annotation.dart';
part 'name.tailor.dart';
To run the code generator, run the following commands:
flutter run build_runner build --delete-conflicting-outputs
Annotate your class with @TailorMixin()
and mix it with generated mixin, generated mixin name starts with _$ following your class name and ending with "TailorMixin" suffix.
Example
import 'package:flutter/material.dart';
import 'package:theme_tailor_annotation/theme_tailor_annotation.dart';
part 'my_theme.tailor.dart';
@TailorMixin()
class MyTheme extends ThemeExtension<MyTheme> with _$MyThemeTailorMixin {
/// You can use required / named / optional parameters in the constructor
// const MyTheme(this.background);
// const MyTheme([this.background = Colors.blue])
const MyTheme({required this.background});
final Color background;
}
The following code snippet defines the "_$MyThemeTailorMixin" theme extension mixin.
- mixin "_$MyThemeTailorMixin" on
ThemeExtension<MyTheme>
- There is getter "background" field of Color type
- Implements "copyWith" from ThemeExtension, with a nullable argument "background" of type "Color"
- Implements "lerp" from ThemeExtension, with the default lerping method for the "Color" type
- Overrites "hashCode" and "==" operator
Additionally theme_tailor_annotation by default generates extension on ThemeData (to change that set themeGetter to ThemeGetter.none or use @TailorMixinComponent
annotation)
- "MyThemeThemeDataProps" extension on "ThemeData" is generated
- getter on "background" of type "Color" is added directly to "ThemeData"
By default, "@tailorMixin" will generate an extension on "ThemeData" and expand theme properties as getters. If this is an undesired behavior, you can disable it by changing the "themeGetter" property in the "@TailorMixin" or using the "@TailorMixinComponent" annotation.
@TailorMixin(themeGetter: ThemeGetter.none)
@TailorMixinComponent() // This automatically sets ThemeGetter.none
"ThemeGetter" has several variants for generating common extensions to ease access to the declared themes.
It might be beneficial to split them into smaller parts, where each part is responsible for the theme of one component. You can think about it as modularization of the theme. ThemeExtensions allow easier custom theme integration with Flutter ThemeData without creating additional Inherited widgets handling theme changes. It is especially beneficial when
- Creating design systems,
- Modularization of the application per feature and components,
- Create a package that supplies widgets and needs more or additional properties not found in ThemeData.
Structure of the application's theme data and its extensions. "chatComponentsTheme" has nested properties.
ThemeData: [] # Flutter's material widgets props
ThemeDataExtensions:
- ChatComponentsTheme:
- MsgBubble:
- Bubble: myBubble
- Bubble: friendsBubble
- MsgList: [foo, bar, baz]
Use "@tailorMixin" / "@TailorMixin" annotations if you may need additional extensions on ThemeData or ThemeContext.
Use "@tailorMixinComponent" / "@TailorMixinComponent" if you intend to nest the theme extension class and do not need additional extensions. Use this annotation for generated themes to allow the generator to recognize the type correctly.
@tailorMixin
class ChatComponentsTheme extends ThemeExtension<ChatComponentsTheme> with _$ChatComponentsTheme {
/// TODO: Implement constructor
final MsgBubble msgBubble;
final MsgList msgList;
final NotGeneratedExtension notGeneratedExtension;
}
@tailorMixinComponent
class MsgBubble extends ThemeExtension<MsgBubble> with _$MsgBubble {
/// TODO: Implement constructor
final Bubble myBubble;
final Bubble friendsBubble;
}
/// The rest of the classes as in the previous example but following @TailorMixin pattern
/// [...]
To see an example implementation of a nested theme, head out to example: nested_themes
ThemeTailor will attempt to provide lerp method for types like:
- Color
- Color?
- TextStyle
- TextStyle?
In the case of an unrecognized or unsupported type, the generator provides a default lerping function (That does not interpolate values linearly but switches between them). You can specify a custom the lerp function for the given type (Color/TextStyle, etc.) or property by extending "ThemeEncoder" class from theme_tailor_annotation
Example of adding custom encoder for an int.
import 'dart:ui';
class IntEncoder extends ThemeEncoder<int> {
const IntEncoder();
@override
int lerp(int a, int b, double t) {
return lerpDouble(a, b, t)!.toInt();
}
}
Use it in different ways:
/// 1 Add it to the encoders list in the @TailorMixin() annotation
@TailorMixin(encoders: [IntEncoder()])
class Theme1 extends ThemeExtension<Theme1> with _$Theme1TailorMixin {}
/// 2 Add it as a separate annotation below @TailorMixin() or @tailorMixin annotation
@tailorMixin
@IntEncoder()
class Theme2 extends ThemeExtension<Theme2> with _$Theme2TailorMixin {}
/// 3 Add it below your custom tailor annotation
const appTailorMixin = TailorMixin(themeGetter: ThemeGetter.onBuildContext);
@appTailorMixin
@IntEncoder()
class Theme3 extends ThemeExtension<Theme3> with _$Theme3TailorMixin {}
/// 4 Add it on the property
@tailorMixin
class Theme4 extends ThemeExtension<Theme4> with _$Theme4TailorMixin {
// TODO constructor required
@IntEncoder()
final Color background;
}
/// 5 IntEncoder() can be assigned to a variable and used as an annotation
/// It works for any of the previous examples
const intEncoder = IntEncoder();
@tailorMixin
@intEncoder
class Theme5 extends ThemeExtension<Theme5> with _$Theme5TailorMixin {}
The generator chooses the proper lerp function for the given field based on the order:
- annotation on the field
- annotation on top of the class
- property from encoders list in the "@TailorMixin" annotation.
Custom-supplied encoders override default ones provided by the code generator. Unrecognized or unsupported types will use the default lerp function.
To see more examples of custom theme encoders implementation, head out to example: theme encoders
To add support for Flutter diagnosticable to the generated ThemeExtension class, import Flutter foundation. Then create the ThemeTailor config class as usual.
import 'package:flutter/foundation.dart';
To see an example of how to ensure debugFillProperties are generated, head out to example: debugFillProperties
For @TailorMixin()
you also need to mix your class with DiagnosticableTreeMixin
@TailorMixin()
class MyTheme extends ThemeExtension<MyTheme>
with DiagnosticableTreeMixin, _$MyThemeTailorMixin {
/// Todo: implement the class
}
The generator will copy all the annotations on the class and the static fields, including: "@JsonSerializable", "@JsonKey", custom JsonConverter(s), and generate the "fromJson" factory. If you wish to add support for the "toJson" method, you can add it in the class extension:
class JsonColorConverter implements JsonConverter<Color, int> {
const JsonColorConverter();
@override
Color fromJson(int json) => Color(json);
@override
int toJson(Color color) => color.value;
}
@tailorMixin
@JsonSerializable()
@JsonColorConverter()
class SerializableTheme extends ThemeExtension<SerializableTheme> with _$SerializableThemeTailorMixin {
SerializableTheme({
required this.fooNumber,
required this.barColor,
});
factory SerializableTheme.fromJson(Map<String, dynamic> json) =>
_$SerializableThemeFromJson(json);
@JsonKey(defaultValue: 10)
final int fooNumber;
@JsonKey()
final Color barColor;
Map<String, dynamic> toJson() => _$SerializableThemeToJson(this);
}
To see an example implementation of "@JsonColorConverter" check out example: json serializable
To serialize nested themes, declare your config classes as presented in the Nesting generated theme extensions, modular themes, design systems. Make sure to use proper json_serializable config either in the annotation on the class or your config "build.yaml" or "pubspec.yaml". For more information about customizing build config for json_serializable head to the json_serializable documentation.
@JsonSerializable(explicitToJson: true)
The generator will use properties from build.yaml or default values for null properties in the @TailorMixin annotation.
Build option | Annotation property | Default | Info |
---|---|---|---|
theme_getter | themeGetter | on_build_context_props | String (ThemeGetter.name): none \ on_theme_data \ on_theme_data_props \ on_build_context \ on_build_context_props |
theme_class_name | themeClassName | null | String For custom Theme if you don't want use Material's Theme. Example: FluentTheme |
theme_data_class_name | themeDataClassName | null | String For custom ThemeData if you don't want use Material's ThemeData FluentThemeData |
targets:
$default:
builders:
theme_tailor:
options:
theme_getter: on_build_context_props
If you're not using Material, feel free to modify the theme_getter extension to another option. For instance, you can use it with a different theme, like FluentTheme.
targets:
$default:
builders:
theme_tailor:
options:
theme_getter: on_build_context_props
theme_class_name: FluentTheme
theme_data_class_name: FluentThemeData
You can configure theme_getter
to generate custom theme extension using 2 properties themeClassName
and themeDataClassName
.
Remember import your theme class.
import 'package:your_theme/your_theme.dart';
- For
ThemeGetter.onBuildContext
andThemeGetter.onBuildContextProps
usethemeClassName
@TailorMixin(
themeGetter: ThemeGetter.onBuildContext,
themeClassName: 'YourTheme'
)
class MyTheme extends ThemeExtension<MyTheme> with _$MyThemeTailorMixin {}
/// The generator will generate an extension:
///
/// extension MyThemeBuildContext on BuildContext {
/// MyTheme get myTheme => YourTheme.of(this).extension<MyTheme>()!;
/// }
- For
ThemeGetter.onThemeData
andThemeGetter.onThemeDataProps
usethemeDataClassName
@TailorMixin(
themeGetter: ThemeGetter.onThemeData,
themeClassName: 'YourThemeData'
)
class MyTheme extends ThemeExtension<MyTheme> with _$MyThemeTailorMixin {}
/// The generator will generate an extension:
///
/// extension MyThemeBuildContext on YourThemeData {
/// MyTheme get myTheme => extension<MyTheme>()!;
/// }
You can also change properties globally by adjusting build.yaml
. Check out Build configuration for more info.
To see an example, head out to example: custom theme getter
Starting from version 2.1.0, the theme_tailor library marks the @Tailor and @TailorComponent annotations as deprecated.
The old theme extension class looked like this:
part 'my_theme.tailor.dart';
@Tailor(
themes: ['light', 'dark'],
)
class $_MyTheme {
static const List<Color> background = [AppColors.white, Colors.black];
}
final light = SimpleTheme.light;
final dark = SimpleTheme.dark;
After migration, your new theme extension class will look like:
part 'my_theme.tailor.dart';
@TailorMixin()
class MyTheme extends ThemeExtension<MyTheme> with _$MyThemeTailorMixin {
MigratedSimpleTheme({required this.background});
final Color background;
}
/// Create themes manually
final lightMyTheme = MyTheme(background: AppColors.white);
final darkMyTheme = MyTheme(background: AppColors.black);
To see an example of how to migrate, head out to example: migration_example