style_generator 0.2.1 copy "style_generator: ^0.2.1" to clipboard
style_generator: ^0.2.1 copied to clipboard

Theme and Style generator. Generates ThemeExtension files

Pub Package

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
before.png after.png

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 #

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 part declaration and with _$[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 :)