theme_tailor 2.0.0 copy "theme_tailor: ^2.0.0" to clipboard
theme_tailor: ^2.0.0 copied to clipboard

Code generator for Flutter's 3.0 ThemeExtension classes. The generator can create themes and extensions on BuildContext or ThemeData based on the lists of the theme properties

Theme Tailor

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.

Table of contents #

Motivation #

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 @Tailor
before after after

Currently the package supports 2 types of the generator:

  • The @TailorMixin annotation generates a mixin with an implementation of the ThemeExtension class. While it requires more boilerplate code than the @Tailor annotation, it offers a more familiar syntax for vanilla ThemeExtension classes and provides greater customization of the created class. This option is recommended for those who prefer a more hands-on approach to their code and require a greater degree of flexibility in their implementation.

  • The @Tailor annotation generates a ThemeExtension class based on the annotated template class and automatically creates instances of the associated themes. This eliminates the need for manual theme creation and consolidates the theme values and their definitions in a single location. However, refactoring properties names of the generated class may prove difficult, and ensuring hot-reloadability of themes may require static const definitions for all properties. Additionally, some lint warnings may need to be ignored since the template class may not be utilized.

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.

How to use #

Install #

ThemeTailor is a code generator and requires build_runner to run. Make sure to add these packages to the project dependencies:

flutter pub add --dev build_runner
flutter pub add --dev theme_tailor
flutter pub add theme_tailor_annotation

Add imports and part directive #

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.

name.dart
import 'package:theme_tailor_annotation/theme_tailor_annotation.dart';

part 'name.tailor.dart';

Run the code generator #

To run the code generator, run the following commands:

flutter run build_runner build --delete-conflicting-outputs

Create Theme class #

@TailorMixin: #

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

my_theme.dart
import 'package:flutter/material.dart';
import 'package:theme_tailor_annotation/theme_tailor_annotation.dart';

part 'my_theme.tailor.dart';

@TailorMixin()
class MyTheme extends ThemeExntension<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;
}

@Tailor: #

ThemeTailor will generate ThemeExtension class based on the configuration class you are required to annotate with theme_tailor_annotation. Please make sure to name class and theme properties appropriately according to the following rules:

  • class name starts with _$ or $_ (Recommendation is to use the former, as it ensures that the configuration class is private). If the class name does not contain the required prefix, then the generated class name will append an additional suffix,
  • class contains static List<T> fields (e.g. static List<Color> surface = []). If no fields exist in the config class, the generator will create an empty ThemeExtension class.

Example

my_theme.dart
import 'package:flutter/material.dart';
import 'package:theme_tailor_annotation/theme_tailor_annotation.dart';

part 'my_theme.tailor.dart';

@tailor
class _$MyTheme {
  static List<Color> background = [Colors.white, Colors.black];
}

The following code snippet defines the "MyTheme" theme extension class.

  • "MyTheme" extends ThemeExtension<MyTheme>
  • The class is immutable with final fields with const constructor
  • There is one field "background" of type Color
  • There are "light" and "dark" static fields matching the default theme names supplied by theme_tailor_annotation
  • 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 BuildContext (to change that set themeGetter to ThemeGetter.none or use @TailorComponent annotation)

  • "MyThemeBuildContextProps" extension on "BuildContext" is generated
  • getter on "background" of type "Color" is added directly to "BuildContext"

Change themes' quantity and names #

@Tailor / @TailorComponent only

By default, "@tailor" will generate two themes: "light" and "dark"; To control the names and quantity of the themes, edit the "themes" property on the "@Tailor" annotation.
You can also change theme names globally by adjusting build.yaml. Check out Build configuration for more info

@Tailor(themes: ['baseTheme'])
class _$MyTheme {}

Access generated themes list #

@Tailor / @TailorComponent only

The generator will create a static getter with a list of the generated themes:

final allThemes = MyTailorGeneratedTheme.themes;

If the themes property is already used in the Tailor class, the generator will use another name and print a warning.

Change generated extensions #

By default, "@tailor" will generate an extension on "BuildContext" and expand theme properties as getters. If this is an undesired behavior, you can disable it by changing the "themeGetter" property in the "@Tailor" or using the "@TailorComponent" annotation.

@Tailor(themeGetter: ThemeGetter.none)
@TailorComponent() // This automatically sets ThemeGetter.none
@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.

Nesting generated ThemeExtensions, Modular themes && DesignSystems #

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 "@tailor" / "@Tailor" or "@tailorMixin" / "@TailorMixin" annotations if you may need additional extensions on ThemeData or ThemeContext.

Use "@tailorComponent"/ "@TailorComponent" or "@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.

Example for @Tailor annotation: #

/// Use generated "ChatComponentsTheme" in ThemeData
@tailor
class _$ChatComponentsTheme {
  @themeExtension
  static List<MsgBubble> msgBubble = MsgBubble.themes;

  @themeExtension
  static List<MsgList> msgList = MsgList.themes;

  /// "NotGeneratedExtension" is a theme extension that is not created using the code generator. It is not necessary to mark it with "@themeExtension" annotation
  static List<NotGeneratedExtension> notGeneratedExtension = [/*custom themes*/];
}

@tailorComponent
class _$MsgBubble {
  // Keep in mind that Bubble type used here may be another Tailor component, and its generated themes can be selectively 
  // assigned to proper fields. (By default tailor will generate two themes: "light" and "dark")

  /// Let's say that my message bubble in 
  /// light mode is darkBlue
  /// dark mode is lightBlue
  @themeExtension
  static List<Bubble> myBubble = [Bubble.darkBlue, Bubble.lightBlue];

  /// Lets say that my message bubble in 
  /// light mode is darkOrange
  /// dark mode is lightOrange
  @themeExtension
  static List<Bubble> friendsBubble = [Bubble.darkOrange, Bubble.lightOrange];
}

@TailorComponent(themes: ['darkBlue', 'lightBlue', 'darkOrange', 'lightOrange'])
class _$Bubble {
  static List<Color> background = [/*Corresponding 'default' values for 'darkBlue', 'lightBlue', 'darkOrange', 'lightOrange'*/];
  static List<Color> textStyle = [/*Corresponding 'default' values for 'darkBlue', 'lightBlue', 'darkOrange', 'lightOrange'*/];
}

/// You can also nest classes marked with @tailor (not recommended)
@tailor
class _$MsgList {
  /// implementation
  /// foo
  /// bar 
  /// baz
}

class NotGeneratedExtension extends ThemeExtension<NotGeneratedExtension> {
  /// implementation
}

Good and bad practices for modular or nested themes when using @Tailor:

/// Good:
@tailorComponent
class _$AppBarTheme {
  static List<Color> foreground = [Colors.white, Colors.white];
}

@tailor
class _$MyTheme {
  @themeExtension
  static List<AppBarTheme> appBarTheme = [AppBarTheme.light, AppBarTheme.dark];
}

/// Bad:
@tailor
class _$MyTheme {
  // This will not be animated and generated theme may result in List<dynamic> property instead of expected List<Color>
  static List<List<Color>> appBarTheme = [
    [Colors.white, Colors.blue],
    [Colors.white, Colors.black]
  ];
} 

Example for @TailorMixin annotation: #

@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

Generate constant themes #

@Tailor and @TailorComponent only, @TailorMixin does not have any limitations regarding creation of constant themes

If the following conditions are met, constant themes will be generated:

  • All List<T> fields are const
  • List length matches theme count
  • List initializers are declared in place, for example:
const someOtherList = ['a','b'];

@Tailor(requireStaticConst: true)
class _$ConstantThemes {
  // This is correct
  static const someNumberList = [1, 2];
  
  // This is bad
  static const otherList = someOtherList;
}

It is possible to force generate constant themes using Tailor(requireStaticConst: true) annotation. In this case fields that do not meet conditions will be excluded from the theme and a warning will be printed.

Hot reload support #

@Tailor and @TailorComponent only, @TailorMixin does not have any limitations regarding hot-reload

To enable hot reload support, use the generateStaticGetters property of the @Tailor() and @TailorComponent annotations. This will generate static getters that allow updating theme properties on hot reload. The getters will conditionally return either the theme itself (if kDebugMode == true) or the final theme otherwise.

To use hot reload with Tailor, make sure to follow these requirements:

  • Import package:flutter/foundation.dart as the option depends on kDebugMode from this package
  • Initialize your theme data in the build method
  • Make sure that properties that should be hot reloadable are either getters or const

Here's an example usage:

import 'package:flutter/foundation.dart';

const lightColor = Color(0xFFA1B2C3);
const darkColor = Color(0xFF123ABC);

@tailor
class _$GetterTheme {
  // This is correct
  static const color1 = [lightColor, darkColor];
  static List<Color> get color2 => [lightColor, darkColor];

  // color3 won't be changed by hot reload as it is not a getter or const
  static List<Color> color3 = [lightColor, darkColor];
}

class GetterPage extends StatelessWidget {
  const GetterPage({super.key});

  final _lightThemeData = ThemeData(extensions: [GetterTheme.light]);

  @override
  Widget build(BuildContext context) {
    final darkThemeData = ThemeData(extensions: [GetterTheme.dark]);

    return MaterialApp(
      // This is correct
      darkTheme: darkThemeData
      // This is correct
      //darkTheme: ThemeData(extensions: [GetterTheme.dark]),

      // This is incorrect for hot reload
      // It will not update as _lightThemeData won't be changed by hot reload
      // (It is necessary to create ThemeData in the build's scope - the same way as `darkThemeData`)
      theme: _lightThemeData,
    );
  }
}

Custom types encoding #

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.

my_theme.dart
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 @Tailor() annotation
@Tailor(encoders: [IntEncoder()])
class _$Theme1 {}

/// 2 Add it as a separate annotation below @Tailor() or @tailor annotation
@tailor
@IntEncoder()
class _$Theme2 {}

/// 3 Add it below your custom tailor annotation
const appTailor = Tailor(themes: ['superLight']);

@appTailor
@IntEncoder()
class _$Theme3 {}

/// 4 Add it on the property
@tailor
class _$Theme4 {
    @IntEncoder()
    static const List<int> someValues = [1,2];
}

/// 5 IntEncoder() can be assigned to a variable and used as an annotation
/// It works for any of the previous examples
const intEncoder = IntEncoder();

@tailor
@intEncoder
class _$Theme5 {}

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 "@Tailor" 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

Flutter diagnosticable / debugFillProperties #

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
}

Json serialization #

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:

@tailor
@JsonSerializable()
class _$SerializableTheme {

  /// This is a custom converter (it will be copied to the generated class)
  @JsonColorConverter()
  static List<Color> foo = [Colors.red, Colors.pink];
}

/// Extension for generated class to support toJson (JsonSerializable does not have to generate this method)
extension SerializableThemeExtension on SerializableTheme {
  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)

Ignore fields #

@Tailor and @TailorComponent only

Fields other than static List<T> are ignored by default by the generator, but if you still want to ignore these, you can use @ignore annotation.

@tailor
class _$IgnoreExample {
  static List<Color> background = [AppColors.white, Colors.grey.shade900];
  @ignore
  static List<Color> iconColor = [AppColors.orange, AppColors.blue];
  @ignore
  static List<int> numbers = [1, 2, 3];
}

Build configuration #

The generator will use properties from build.yaml or default values for null properties in the @Tailor annotation.

Build option Annotation property Default Info
themes themes [light, dark] List ([] empty array -> no themes generated)
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
require_static_const requireStaticConst false bool
targets:
  $default:
    builders:
      theme_tailor:
        options:
          themes: [light, dark, amoled]
          theme_getter: on_build_context_props
          require_static_const: false
149
likes
0
pub points
89%
popularity

Publisher

verified publisheriteo.com

Code generator for Flutter's 3.0 ThemeExtension classes. The generator can create themes and extensions on BuildContext or ThemeData based on the lists of the theme properties

Repository (GitHub)
View/report issues

Documentation

Documentation

License

unknown (LICENSE)

Dependencies

analyzer, build, build_config, collection, json_annotation, meta, source_gen, source_gen_test, source_helper, theme_tailor_annotation

More

Packages that depend on theme_tailor