style_generator 0.2.6
style_generator: ^0.2.6 copied to clipboard
Theme and Style generator. Generates ThemeExtension files
This package is considered stable and will jump to version 1.0.0 after a testing period.
Generates ThemeExtensions for your Style Classes
| Before | After |
|---|---|
![]() |
![]() |
Getting Started #
dart pub add style_generator_annotation
dart pub add dev:style_generator
dart pub add dev:build_runner
Template Plugin #
For even easier generation, use the Style Generator Templates for Flutter Plugin for Android Studio
Index #
- Getting Started
- Template Plugin
- Index
- ThemeExtensions
- Annotations
- Prefixed Imports and static callbacks
- Feedback
ThemeExtensions #
Minimum Example: #
import 'package:flutter/material.dart';
// add import
import 'package:style_generator_annotation/style_generator_annotation.dart';
// add part file: your_file_name.style.dart
part 'some_style.style.dart';
// add Style annotation and Mixin _$YourClass
@Style()
class SomeStyle extends ThemeExtension<SomeStyle> with _$SomeStyle {
// just add the fields and a constructor to assign them
final TextStyle? titleStyle;
final TextStyle? subtitleStyle;
final Color? color;
final Color? selectionColor;
const SomeStyle({this.titleStyle, this.color, this.subtitleStyle, this.selectionColor});
// factory SomeStyle.of(BuildContext context, [SomeStyle? style]) => _$SomeStyleOf(context, style); // <-- will generate even more functionality
}
Fallback and of() Constructor: #
This package supports generating a quick constructor to retrieve your Style from BuildContext.
import "package:flutter/widgets.dart";
class SomeWidget extends StatelessWidget {
final SomeStyle? style;
const SomeWidget({super.key, this.style});
@override
Widget build(BuildContext context) {
// retrieve your custom style from context, backed by your SomeStyle.fallback() design.
SomeStyle s = SomeStyle.of(context, style);
return const Placeholder();
}
}
class SomeStyle extends ThemeExtension<SomeStyle> with _$SomeStyle {
// ... see minimal example at the top
factory SomeStyle.fallback(BuildContext context, {String? something}) {
ThemeData theme = Theme.of(context);
ColorScheme scheme = theme.colorScheme;
TextTheme textTheme = theme.textTheme;
return SomeStyle(
titleStyle: textTheme.titleSmall,
subtitleStyle: textTheme.bodySmall,
color: scheme.secondaryContainer,
selectionColor: scheme.primaryContainer,
);
}
// add YourClass.of(BuildContext context) as a factory constructor
factory SomeStyle.of(BuildContext context, [SomeStyle? style]) => _$SomeStyleOf(context, style);
// That will generate this method
SomeStyle _$SomeStyleOf(BuildContext context, [SomeStyle? style]) {
SomeStyle s = SomeStyle.fallback(context);
s = s.merge(Theme.of(context).extension<SomeStyle>());
s = s.merge(style);
return s;
}
}
Positional and Named Parameter #
A mix of positional and named parameter are supported.
@Style()
class SomeStyle extends ThemeExtension<SomeStyle> with _$SomeStyle {
final TextStyle? titleStyle;
final TextStyle? subtitleStyle;
final Color? color;
final Color? selectionColor;
const SomeStyle(this.titleStyle, this.color, {this.subtitleStyle, this.selectionColor});
}
Additionally non-nullable parameter and typed parameter are also supported.
@Style()
class SomeStyle extends ThemeExtension<SomeStyle> with _$SomeStyle {
final TextStyle titleStyle;
final TextStyle subtitleStyle;
// Types that have a lerp function, whether static or not are used for lerp() automatically.
// Here, the lerp() method from Some<T, K> will be used.
final Some<Color, double>? color;
final Color? selectionColor;
const SomeStyle(this.titleStyle, this.color, {required this.subtitleStyle, this.selectionColor});
}
class Some<T, K> {
final T color;
final K something;
const Some(this.color, this.something);
Some<T, K>? lerp(Some<T, K>? a, Some<T, K>? b, double t) {
return b;
}
}
Annotations #
Style #
The Style annotation allows customizing the generation of the style class.
For example, you can disable the `copyWith()` generation and use another package for it.See the Style annotation documentation for more info.
@Style(constructor: "_", fallback: "custom", genCopyWith: true, genMerge: true, genLerp: true, genOf: true, suffix: "Generated")
class SomeStyle extends ThemeExtension<SomeStyle> with _$SomeStyleGenerated {
final TextStyle titleStyle;
final TextStyle subtitleStyle;
// lerp method from Some<T, K> will be used
final Some<Color, double>? color;
final Color? selectionColor;
const SomeStyle(this.titleStyle, this.color, {required this.subtitleStyle, this.selectionColor});
factory SomeStyle.of(BuildContext context, [SomeStyle? style]) => _$SomeStyleGeneratedOf(context, style);
}
Customize the default behavior with build.yml #
You can customize the default behavior in your build.yml
targets:
$default:
builders:
style_generator|style_builder:
enabled: true
options:
constructor: null # The constructor for .copyWith and .lerp to use. The default is `null`
fallback: "fallback" # The constructor for .of to use, The default is `null`
gen_copy_with: true # whether .copyWith should be generated. The default is `true`
gen_merge: true # whether .merge should be generated. The default is `true`
gen_lerp: true # whether .lerp should be generated. The default is `true`
gen_of: null # whether .of should be generated. The default is `null` (will only generate if the fallback constructor and a factory .of() constructor is found)
suffix: null # an optional suffix for the generated mixin and .of method. The default is `null`
StyleKey<T> #
Every field and constructor parameter can be further configured with @StyleKey<T>()
@Style()
class SomeStyle extends ThemeExtension<SomeStyle> with _$SomeStyle {
final TextStyle? titleStyle;
final TextStyle? subtitleStyle;
@StyleKey(inCopyWith: false, inLerp: false, inMerge: false)
final Color? color;
final Color? selectionColor;
const SomeStyle({
this.titleStyle,
this.color,
this.subtitleStyle,
@StyleKey(inCopyWith: false, inLerp: false, inMerge: false)
this.selectionColor,
});
}
Only StyleKeys on fields and the constructor matching Style.constructor are considered.
Do note, StyleKeys on the constructor override configurations on the field without further warning.
Custom Lerp Functions #
Sometimes, custom lerp functions are required.
The generator will consider lerpDouble() for integer, double and num fields by default. It uses a lerpDuration for Darts Duration class. For all other cases, it searches for a .lerp() method inside the types class and applies that. If nothing is found, .lerp() will not lerp the field (and a warning is printed).
Other custom typed .lerp() functions can be applied with StyleKeys. (The custom method must be static or a top level method).
@Style()
class SomeStyle extends ThemeExtension<SomeStyle> with _$SomeStyle {
final TextStyle? titleStyle;
final TextStyle? subtitleStyle;
// The function must match the type of the StyleKey. (Which itself should match the type of the field)
@StyleKey<WidgetStateProperty<double?>?>(lerp: lerpWidgetStateProperty)
final WidgetStateProperty<double?>? elevation;
// Here, a custom color lerp function is used
@StyleKey<Color?>(lerp: customColorLerp)
final Color? color;
final Color? selectionColor;
const SomeStyle({
this.titleStyle,
this.color,
this.elevation,
this.subtitleStyle,
this.selectionColor,
});
static WidgetStateProperty<double?>? lerpWidgetStateProperty(
WidgetStateProperty<double?>? a,
WidgetStateProperty<double?>? b,
double t,
) {
return WidgetStateProperty.lerp<double?>(a, b, t, lerpDouble);
}
}
Color? customColorLerp(Color? a, Color? b, double t) => b;
Custom Merge Functions #
Sometimes, custom merge functions are required.
For example, if you don't want to apply the default merge() behavior of a TextStyle.
(The custom method must be static or a top level method).
@Style()
class SomeStyle extends ThemeExtension<SomeStyle> with _$SomeStyle {
// The function must match the type of the StyleKey. (Which itself should match the type of the field)
@StyleKey<TextStyle?>(merge: noMerge)
final TextStyle? titleStyle;
final TextStyle? subtitleStyle;
final Color? color;
final Color? selectionColor;
const SomeStyle({
this.titleStyle,
this.color,
this.subtitleStyle,
this.selectionColor,
});
}
CopyWith #
Since we already generating a `copyWith()` for `ThemeExtension`s, I've added a standalone `@CopyWith()` annotation that generates standalone `copyWith()` Extensions (or Mixins)
Since it comes with a different import, it won't collide with other packages when only the @Style() annotation is used
import 'package:style_generator_annotation/copy_with_generator_annotation.dart';
@CopyWith()
class Profile {
final String firstname;
final String lastname;
String get name => "$firstname $lastname";
final DateTime? birthday;
int get age => birthday != null ? DateTime.now().difference(birthday!).inDays ~/ 365 : 0;
const Profile({this.firstname = "", this.lastname = "", this.birthday});
}
which generates:
import 'package:test_generation/src/data/profile.dart';
extension $ProfileExtension on Profile {
Profile copyWith({
String? firstname,
String? lastname,
DateTime? birthday,
}) {
return Profile.new(
firstname: firstname ?? this.firstname,
lastname: lastname ?? this.lastname,
birthday: birthday ?? this.birthday,
);
}
}
CopyWith as Mixin #
`copyWith()` as a Mixin is possible but comes with some drawbacks
- More boilerplate for you (adding the
partdeclaration andwith _$[ClassName]modifier manually) - Subclasses won't be able to override the
copyWith()method if their parameters differ
import 'package:style_generator_annotation/copy_with_generator_annotation.dart';
part 'some_user.copy_with.dart';
@CopyWith(asExtension: false)
class SomeUser with _$SomeUser {
final String firstname;
final String lastname;
final DateTime? birthday;
const SomeUser(this.firstname, this.lastname, this.birthday);
}
which generates:
part of "some_user.dart";
mixin _$SomeUser {
String get firstname;
String get lastname;
DateTime? get birthday;
SomeUser copyWith({String? firstname, String? lastname, DateTime? birthday}) {
return SomeUser.new(
firstname ?? this.firstname,
lastname ?? this.lastname,
birthday ?? this.birthday,
);
}
}
Customize the default behavior with build.yml #
You can customize the default behavior in your build.yml
targets:
$default:
builders:
style_generator|copy_with_builder:
enabled: true
options:
constructor: null # The constructor for .copyWith to use. The default is `null`
asExtension: null # whether an Extension method or a mixin should be generated. The default is `null` (will generate an Extension)
suffix: null # an optional suffix for the generated extension or mixin. The default is `null`
CopyWithKey #
The generation of the `copyWith()` method can be further customized
a more complex example:
import 'package:style_generator_annotation/copy_with_generator_annotation.dart';
import 'package:test_generation/src/question_style.dart';
import '../fake.dart' as fake;
class Profile {
final String firstname;
@CopyWithKey(inCopyWith: false) // this will hide lastname in subclasses too
final String lastname;
String get name => "$firstname $lastname";
final DateTime? birthday;
int get age => birthday != null ? DateTime.now().difference(birthday!).inDays ~/ 365 : 0;
const Profile({
@CopyWithKey(inCopyWith: false) this.firstname = "", // this will hide firstname only it its own class
this.lastname = "",
this.birthday,
});
}
class UserProfile extends Profile {
final String id;
final String firstName; // don't get confused with 'firstName' and 'firstname'
final fake.TextStyle? style;
final QuestionStyle? question;
UserProfile({
this.id = "",
this.firstName = "",
super.lastname, // this is hidden through its super class, annotate it in the constructor with @CopyWithKey(inCopyWith: true) to override the behavior
this.style,
this.question,
super.birthday,
super.firstname,
});
}
The generated code adds all necessary imports and comments out what is not included in your copyWith()
import 'dart:core';
import 'package:test_generation/src/fake.dart' as fake;
import 'package:test_generation/src/question_style.dart';
import 'package:test_generation/src/data/profile.dart';
extension $ProfileExtension on Profile {
Profile copyWith({
// String? firstname,
// String? lastname,
DateTime? birthday,
}) {
return Profile.new(
// firstname: firstname ?? this.firstname,
// lastname: lastname ?? this.lastname,
birthday: birthday ?? this.birthday,
);
}
}
extension $UserProfileExtension on UserProfile {
UserProfile copyWith({
String? id,
String? firstName,
fake.TextStyle? style, // prefixed imports are supported
QuestionStyle? question,
// String? lastname,
DateTime? birthday,
String? firstname,
}) {
return UserProfile.new(
id: id ?? this.id,
firstName: firstName ?? this.firstName,
style: style ?? this.style,
question: question ?? this.question,
// lastname: lastname ?? this.lastname,
birthday: birthday ?? this.birthday,
firstname: firstname ?? this.firstname,
);
}
}
Equality #
generates `hashCode` and `operator ==` functions
Just like the @CopyWith annotation, it comes with its own import and won't collide with other packages
when only the @Style() annotation is used
import 'package:style_generator_annotation/equality_generator_annotation.dart';
part 'some_user.equality.dart';
@Equality()
class SomeUser with _$SomeUserEquality {
final String firstname;
final String lastname;
final DateTime? birthday;
const SomeUser(this.firstname, this.lastname, this.birthday);
}
which generates:
part of "some_user.dart";
mixin _$SomeUserEquality {
String get firstname;
String get lastname;
DateTime? get birthday;
List<Profile>? get profiles;
int get hashCode => Object.hashAll([
firstname,
lastname,
birthday,
const DeepCollectionEquality().hash(profiles),
]);
bool operator ==(Object other) {
if (other is! SomeUser) return false;
return identical(this, other) ||
firstname == other.firstname &&
lastname == other.lastname &&
birthday == other.birthday &&
const DeepCollectionEquality().equals(profiles, other.profiles);
}
}
Customize the default behavior with build.yml #
You can customize the default behavior in your build.yml
targets:
$default:
builders:
style_generator|equality_builder:
enabled: true
options:
constructor: null # The constructor to lookup required fields. The default is `null`
suffix: "Equality" # an optional suffix for the generated mixin. The default is `Equality`
EqualityKey #
The generation of the `hashCode` and `operator ==` method can be further customized
- fields can be excluded from
hashCodealone - fields can be excluded from
operator ==alone
Excluding fields from only one method is discouraged, consider using @EqualityKey.exclude to
exclude the field from both (or include them again when they were excluded in a parent class)
import 'package:style_generator_annotation/equality_generator_annotation.dart';
part 'eq_parent.equality.dart';
@Equality()
class EqParent with _$EqParentEquality {
final EqParent zero;
@EqualityKey(inEquals: false, inHash: false)
final String one;
@EqualityKey(inHash: true)
final int two;
final List<String> list;
@EqualityKey.exclude()
final HashMap<String, bool> hashMap;
final Set<int> set;
const EqParent(this.zero, this.one, this.two, this.list, this.hashMap, this.set);
}
@Equality()
class EqChild extends EqParent with _$EqChildEquality {
final EqChild ten;
final String eleven;
final List<Object?> oList;
final List<double?>? nullableList;
final List<dynamic> dynList;
final dynamic dyn;
final Map<dynamic, List<dynamic>> dynMap;
const EqChild(
super.zero,
super.one,
super.two,
super.list,
this.ten,
@EqualityKey.exclude(false) super.hashMap,
super.set,
this.eleven,
this.oList,
this.nullableList,
this.dynList,
this.dyn,
this.dynMap,
);
}
which generates
part of "eq_parent.dart";
mixin _$EqParentEquality {
EqParent get zero;
String get one;
int get two;
List<String> get list;
HashMap<String, bool> get hashMap;
Set<int> get set;
int get hashCode => Object.hashAll([
zero,
// one,
two,
const DeepCollectionEquality().hash(list),
// const DeepCollectionEquality().hash(hashMap),
const DeepCollectionEquality().hash(set),
]);
bool operator ==(Object other) {
if (other is! EqParent) return false;
return identical(this, other) ||
zero == other.zero
// && one == other.one
&&
two == other.two &&
const DeepCollectionEquality().equals(list, other.list)
// && const DeepCollectionEquality().equals(hashMap, other.hashMap)
&&
const DeepCollectionEquality().equals(set, other.set);
}
}
mixin _$EqChildEquality {
EqParent get zero;
String get one;
int get two;
List<String> get list;
EqChild get ten;
HashMap<String, bool> get hashMap;
Set<int> get set;
String get eleven;
List<Object?> get oList;
List<double?>? get nullableList;
List<dynamic> get dynList;
dynamic get dyn;
Map<dynamic, List<dynamic>> get dynMap;
int get hashCode => Object.hashAll([
zero,
// one,
two,
const DeepCollectionEquality().hash(list),
ten,
const DeepCollectionEquality().hash(hashMap),
const DeepCollectionEquality().hash(set),
eleven,
const DeepCollectionEquality().hash(oList),
const DeepCollectionEquality().hash(nullableList),
const DeepCollectionEquality().hash(dynList),
dyn,
const DeepCollectionEquality().hash(dynMap),
]);
bool operator ==(Object other) {
if (other is! EqChild) return false;
return identical(this, other) ||
zero == other.zero
// && one == other.one
&&
two == other.two &&
const DeepCollectionEquality().equals(list, other.list) &&
ten == other.ten &&
const DeepCollectionEquality().equals(hashMap, other.hashMap) &&
const DeepCollectionEquality().equals(set, other.set) &&
eleven == other.eleven &&
const DeepCollectionEquality().equals(oList, other.oList) &&
const DeepCollectionEquality().equals(
nullableList,
other.nullableList,
) &&
const DeepCollectionEquality().equals(dynList, other.dynList) &&
dyn == other.dyn &&
const DeepCollectionEquality().equals(dynMap, other.dynMap);
}
}
Prefixed Imports and static callbacks #
If you use prefixed imports, one restriction apply.
(1)
If dart:ui is imported with a prefix (e.g. import 'dart:ui' as ui)
AND you use the Dart-Core data types int, num or double,
automatically lerping them fails (because lerpDouble is not found).
Consider adding a top level function lerpDouble (like in the example below)
or apply a StyleKey(lerp: ...) manually.
(2) Otherwise, all weird combinations should work (if not, the generator will tell you).
(3) If you use static callbacks, always write their full scope.
import 'dart:ui' as ui; // <-- prefixed import
import 'dart:core' as core; // <-- prefixed import
import 'package:flutter/material.dart';
import 'package:style_generator_annotation/style_generator_annotation.dart';
part 'some_style.style.dart';
const lerpDouble = ui.lerpDouble; // <-- explained above in (1)
@Style()
class SomeStyle extends ThemeExtension<SomeStyle> with _$SomeStyle {
final core.Duration? animationDuration;
final core.double? something; // will use lerpDouble which redirects to ui.lerpDouble
@StyleKey<core.double?>(lerp: ui.lerpDouble) // <-- alternative explained above in (1)
final core.double? somewhere;
@StyleKey<Color?>(lerp: SomeStyle.alwaysRed) //don't write `alwaysRed` only, the mixin won't find it
final Color? color;
const SomeStyle({this.animationDuration, this.something, this.somewhere, this.color});
static Color? alwaysRed(Color? a, Color? b, core.double t) => Colors.red;
}
Full build.yaml example #
This build.yaml shows the default configuration of all annotations
You can create a build.yaml inside your project (on the same level as your pubspec.yaml)
and insert and modify this content to change the default behavior project wide.
Note: setting enable: false should not be necessary. All builder come with their own imports
and therefor only trigger when their import is found. They will not interfere with other packages.
targets:
$default:
builders:
style_generator|style_builder:
enabled: true
options:
constructor: null
fallback: null
gen_copy_with: true
gen_merge: true
gen_lerp: true
gen_of: null
suffix: null
style_generator|copy_with_builder:
enabled: true
options:
constructor: null
asExtension: null
suffix: null
style_generator|equality_builder:
enabled: true
options:
constructor: null
suffix: "Equality"
Feedback #
Any feedback is welcome :)

