Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for other theme extensions in class fields #10

Merged
merged 12 commits into from Jun 20, 2022

Conversation

jakubtiteo
Copy link
Contributor

@jakubtiteo jakubtiteo commented Jun 13, 2022

Example:

This three themes:

@tailor
class $_SomeTheme {
  static List<Color> appBackground = [AppColors.white, AppColors.black];
  @themeExtension
  static List<AnotherThemePart> anotherThemePart = [AnotherThemePart.light, AnotherThemePart.dark];
  static List<OtherThemeExtension> otherThemeExtension = [OtherThemeExtension(), OtherThemeExtension()];
}

@tailorComponent
class $_AnotherThemePart {
  static List<Color> navBarBackground = [AppColors.white, AppColors.black];
}

class OtherThemeExtension extends ThemeExtension<OtherThemeExtension> {
  OtherThemeExtension({
    this.floatingActionButtonColor = Colors.white,
  });

  final Color floatingActionButtonColor;
  
  // ...
}

Will result in following lerp method

  @override
  SomeTheme lerp(ThemeExtension<SomeTheme>? other, double t) {
    if (other is! SomeTheme) return this;
    return SomeTheme(
      appBackground: Color.lerp(appBackground, other.appBackground, t)!,
      anotherThemePart: anotherThemePart.lerp(other.anotherThemePart, t),
      otherThemeExtension:
          otherThemeExtension.lerp(other.otherThemeExtension, t),
    );
  }

This PR also adds generated lists of themes, example:

@tailor
class $_SomeTheme {
  @themeExtension
  static List<AnotherThemePart> anotherThemePartGeneratedConstructor = AnotherThemePart.themes;
}

@tailorComponent
class $_AnotherThemePart {
  static List<Color> navBarBackground = [AppColors.white, AppColors.black];
}

Generated code:

class AnotherThemePart extends ThemeExtension<AnotherThemePart> {
  static final themes = [
    dark,
    light,
  ];
}

# Conflicts:
#	packages/theme_tailor/lib/src/generator/theme_tailor_generator.dart
#	packages/theme_tailor/lib/src/template/theme_class_template.dart
@Rongix
Copy link
Collaborator

Rongix commented Jun 13, 2022

I thought we could support nested themes like this:

@tailor
class $_AppTheme {
  List<CustomAppBarTheme> appBarTheme = [
    CustomAppBarTheme.otherLight,
    CustomAppBarTheme.otherDark
  ];
}

@Tailor(themes: ['otherLight', 'otherDark'])
class $_CustomAppBarTheme {
  List<Color> surface = [Colors.blue, Colors.deepPurple];
}

If one of the InterfaceType is ThemeExtension then we can simply use its copyWith and lerp.
WDYT?

Also ideally if this is a part of another theme extension class we would like to suppress it's generated extensions
so we might add smth

so maybe smth like that:

class TailorComponent extends Tailor {
  const TailorComponent({
    List<String> themes = const ['light', 'dark'],
    List<ThemeEncoder> encoders = const [],
  }) : super(themes: themes, encoders: encoders, isPart: true, themeGetter: ThemeGetter.none);
}

@TailorComponent()
class $_AnotherTheme {
  static List<Color> navBarBackground = [AppColors.white, AppColors.black];
}

@jakubtiteo
Copy link
Contributor Author

Ok, I will implement generating code that uses copyWith and lerp from classes that implements ThemeExtension.
Maybe we can keep automatic constructor generation by using explicitly empty list, or something like this?

@tailor
class $_AppTheme {
  @inheritTailorTheme
  List<CustomAppBarTheme> appBarTheme = [];
}

@tailorComponent
class $_CustomAppBarTheme {
  List<Color> surface = [Colors.blue, Colors.deepPurple];
}

@Rongix
Copy link
Collaborator

Rongix commented Jun 13, 2022

Hm I think it would be preferable to generate list of available themes and supply it instead of the empty list. By default empty list would mean that no theme will be generated

  List<CustomAppBarTheme> appBarTheme = CustomAppBarTheme.themes;

(I'm not sure about the annotation on the "appBarTheme" - maybe we could name it just @component)

Copy link
Collaborator

@Rongix Rongix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TailorComponent -> Just Tailor() but won't ever generate any extensions, it is meant for using in another @Tailor or if someone does not want extensions

@Component / @themeExtension -> Annotation only for field. Hints generator that the type conforms to ThemeExtension and can call lerp() on it.
(When checking for annotation type, can use "const themeExtensionChecker = TypeChecker.fromRuntime(Extension);"

/// (When exporting only show themeExtension to not conflict with Flutter)
const themeExtension = ThemeExtension();

class ThemeExtension {
  const ThemeExtension();
}

Generated copyWith -> Not sure about this one, I would keep it simple for now and not worry about handling deep copy

We can't use smth like: "$AnotherThemePart" or "$AnotherThemePart" because this type might be private somewhere also it does not look super clean

@tailor
class $_SomeTheme {
static List<Color> appBackground = [AppColors.white, AppColors.black];
static List<$_AnotherThemePart> anotherThemePart = [AnotherThemePart.light, AnotherThemePart.dark];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static List<$_AnotherThemePart> anotherThemePart = [AnotherThemePart.light, AnotherThemePart.dark];
static List<AnotherThemePart> anotherThemePart = [AnotherThemePart.light, AnotherThemePart.dark];

It is because we can't determine the type before it is generated?
Once more I suggest adding annotation directly on top of the field:

Comment on lines 79 to 87
if (value.isThemeExtension) {
classParams.write('$key: this.$key.copyWith(');
for (final extensionField in value.themeExtensionFields) {
classParams.write('$extensionField: $key?.$extensionField,');
}
classParams.write('),');
} else {
classParams.write('$key: $key ?? this.$key,');
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like it currently copies whole object anyways, therefore can be treated as any other field

Comment on lines 79 to 87
if (value.isThemeExtension) {
classParams.write('$key: this.$key.copyWith(');
for (final extensionField in value.themeExtensionFields) {
classParams.write('$extensionField: $key?.$extensionField,');
}
classParams.write('),');
} else {
classParams.write('$key: $key ?? this.$key,');
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (value.isThemeExtension) {
classParams.write('$key: this.$key.copyWith(');
for (final extensionField in value.themeExtensionFields) {
classParams.write('$extensionField: $key?.$extensionField,');
}
classParams.write('),');
} else {
classParams.write('$key: $key ?? this.$key,');
}
classParams.write('$key: $key ?? this.$key,');

Comment on lines 140 to 147
class _FieldListingVisitor extends SimpleElementVisitor<Object> {
final List<String> fieldNames = [];

@override
void visitFieldElement(FieldElement element) {
fieldNames.add(element.name);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we change to the same copyWith as other fields we don't need it

Suggested change
class _FieldListingVisitor extends SimpleElementVisitor<Object> {
final List<String> fieldNames = [];
@override
void visitFieldElement(FieldElement element) {
fieldNames.add(element.name);
}
}

Comment on lines 132 to 134
isAnotherTailorTheme: hasTailorComponentAnnotation,
isThemeExtension: isThemeExtension,
themeExtensionFields: themeExtensionFields,
Copy link
Collaborator

@Rongix Rongix Jun 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say if there is annotation @themeExtension on the field we can assume it conforms to ThemeExtension<T> interface therefore can safely call lerp on it. I can see that it can be a bit problematic to determine type of class that is not yet generated (I think it's good idea to add annotation of field directly)

Suggested change
isAnotherTailorTheme: hasTailorComponentAnnotation,
isThemeExtension: isThemeExtension,
themeExtensionFields: themeExtensionFields,
isThemeExtension: isThemeExtension,

When it comes to the 'isAnotherTailorTheme' it should not matter 🤔

Comment on lines 110 to 114
if (isThemeExtension) {
final fieldListingVisitor = _FieldListingVisitor();
coreType.element!.visitChildren(fieldListingVisitor);
themeExtensionFields.addAll(fieldListingVisitor.fieldNames);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (isThemeExtension) {
final fieldListingVisitor = _FieldListingVisitor();
coreType.element!.visitChildren(fieldListingVisitor);
themeExtensionFields.addAll(fieldListingVisitor.fieldNames);
}


final isThemeExtension = extendsThemeExtension || hasTailorComponentAnnotation;

final themeExtensionFields = <String>[];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final themeExtensionFields = <String>[];

@tailor
class $_SomeTheme {
static List<Color> appBackground = [AppColors.white, AppColors.black];
static List<$_AnotherThemePart> anotherThemePart = [AnotherThemePart.light, AnotherThemePart.dark];
Copy link
Collaborator

@Rongix Rongix Jun 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static List<$_AnotherThemePart> anotherThemePart = [AnotherThemePart.light, AnotherThemePart.dark];
@themeExtension
static List<AnotherThemePart> anotherThemePart = [AnotherThemePart.light, AnotherThemePart.dark];

maybe even themeExtension will be better

Copy link
Collaborator

@Rongix Rongix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must fix:
If list type is nullable is is encoded as non-nullable in theme extension (Previously we were using .getDisplayString(withNullability: true))

@Tailor
class $_SomeTheme {
static List<Color?> appBackground = [AppColors.white, AppColors.black];
}

Optionally to change:

  1. Run _TailorClassVisitor first, get info which fields are marked with '@themeExtension'
  2. Pass these fields names to _ListFieldTypeASTVisitor and get types

import 'package:meta/meta_meta.dart';
import 'package:theme_tailor_annotation/theme_tailor_annotation.dart';

const tailorComponent = TailorComponent();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs documentation (Now I see that Tailor also lacks docs :O)
Maybe:
/// Default instance of [TailorComponent] annotation


const tailorComponent = TailorComponent();

@Target({TargetKind.classType})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can add docs here, maybe smth like:
"An annotation used to specify that generated ThemeExtension class can be used as a part of other ThemeExtension class and does not need additional extensions on BuildContext nor ThemeData for the ease of access"

@@ -3,3 +3,5 @@ library theme_tailor_annotation;
export 'src/theme_encoder.dart';
export 'src/theme_getter.dart';
export 'src/theme_tailor.dart';
export 'src/theme_tailor_component.dart';
export 'src/theme_extension.dart';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No newline at end of file (please add one)

}
}

class ListFieldTypeASTVisitor extends SimpleAstVisitor {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be private

Comment on lines 109 to 113
final extendsThemeExtension = coreType.typeImplementations
.firstWhereOrNull((t) => t
.getDisplayString(withNullability: false)
.startsWith('ThemeExtension')) !=
null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final extendsThemeExtension = coreType.typeImplementations
.firstWhereOrNull((t) => t
.getDisplayString(withNullability: false)
.startsWith('ThemeExtension')) !=
null;
final extendsThemeExtension = coreType.typeImplementations.any((e) => e
.getDisplayString(withNullability: false)
.startsWith('ThemeExtension'));

Co-authored-by: Rongix <pawel.franitza@iteo.com>
@Rongix Rongix merged commit 333952b into main Jun 20, 2022
@Rongix Rongix deleted the feature/other_extensions_in_theme_class_fields branch June 29, 2022 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants