style_generator 0.2.1
style_generator: ^0.2.1 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,
);
}
}
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;
}
Feedback #
Any feedback is welcome :)

