Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ Or add to `pubspec.yaml`:

```yaml
dependencies:
theme_extensions_builder_annotation: ^7.3.0
theme_extensions_builder_annotation: ^7.5.0

dev_dependencies:
build_runner: ^2.13.0
theme_extensions_builder: ^7.3.0
theme_extensions_builder: ^7.5.0
```

## 🚀 Quick Start
Expand Down Expand Up @@ -143,6 +143,18 @@ Check out the [example project](../packages/theme_extensions_builder/example) fo
- **Custom Components**: Buttons, cards, and typography showcases
- **Best Practices**: Real-world organization patterns

## 🛠️ Development

The repository is a [pub workspace](https://dart.dev/tools/pub/workspaces): one `flutter pub get` at the root resolves every package, including the example app.

```bash
flutter pub get
scripts/prepare_push.sh # format, analyze, build and test every package
scripts/update_goldens.sh # regenerate the golden files after a generator change
```

The generator tests run on the Dart SDK alone. The Flutter classes the fixtures use are small stand-ins in `packages/theme_extensions_builder/test/fixtures/flutter_stubs.dart`, with the same `lerp` and `merge` signatures as the real ones. The example app is where the generated code meets the real framework, so CI regenerates and analyzes it on every push.

## 📄 License

MIT License - see the [LICENSE](../LICENSE) file for details.
110 changes: 87 additions & 23 deletions .github/workflows/dart.yml
Original file line number Diff line number Diff line change
@@ -1,43 +1,107 @@
name: Dart CI
name: CI

on:
# workflow_dispatch: # Only manual trigger
push:
branches: [ "main" ]
branches: [main]
pull_request:
branches: [ "main" ]
branches: [main]

env:
BUILDER: packages/theme_extensions_builder
ANNOTATION: packages/theme_extensions_builder_annotation
EXAMPLE: packages/theme_extensions_builder/example

jobs:
test:
name: Test (${{ matrix.deps }} dependencies)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# The builder advertises a range of analyzer versions; the lower
# bound is exercised by pinning the analyzer stack to it.
deps: [latest, lowest]

steps:
- uses: actions/checkout@v4

- uses: dart-lang/setup-dart@v1
# The example app is a workspace member, so the workspace is resolved
# with the Flutter SDK. Everything else runs on the Dart SDK it bundles.
- uses: subosito/flutter-action@v2
with:
sdk: stable
channel: stable
cache: true

- name: Install dependencies (annotation)
working-directory: packages/theme_extensions_builder_annotation
run: dart pub get --no-example
- name: Resolve the workspace
run: flutter pub get

- name: Run formatter check (annotation)
working-directory: packages/theme_extensions_builder_annotation
run: dart format --set-exit-if-changed .
# `dart pub downgrade analyzer ...` changes nothing at the workspace
# root, and a plain `dart pub downgrade` also takes the test runner to
# a version that no longer runs on the current SDK. The lower bounds
# are pinned outright instead; everything else resolves around them.
#
# The analyzer floor is the lowest version the test tooling compiles
# against: every `dart_style` the goldens are formatted with needs
# 13.1.0. `build` floats, as the tooling needs 4.x.
- name: Pin the lower bounds
if: matrix.deps == 'lowest'
run: |
cat >> pubspec.yaml <<'EOF'
dependency_overrides:
analyzer: 13.1.0
source_gen: 4.2.3
EOF
flutter pub get

- name: Run tests (annotation)
working-directory: packages/theme_extensions_builder_annotation
run: dart test
- name: Check formatting
if: matrix.deps == 'latest'
run: dart format --set-exit-if-changed $BUILDER $ANNOTATION

- name: Install dependencies (builder)
working-directory: packages/theme_extensions_builder
run: dart pub get --no-example
- name: Analyze
run: dart analyze --fatal-infos $BUILDER $ANNOTATION

- name: Run formatter check (builder)
working-directory: packages/theme_extensions_builder
run: dart format --set-exit-if-changed .
- name: Test annotation
working-directory: ${{ env.ANNOTATION }}
run: dart test

- name: Run tests (builder)
working-directory: packages/theme_extensions_builder
- name: Test builder
working-directory: ${{ env.BUILDER }}
run: dart test

generated:
name: Generated files are up to date
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Resolve the workspace
run: flutter pub get

# The runtime tests import the committed generated files, so a stale
# file would test yesterday's generator.
- name: Regenerate the builder fixtures
working-directory: ${{ env.BUILDER }}
run: dart run build_runner build

- name: Regenerate the example
working-directory: ${{ env.EXAMPLE }}
run: dart run build_runner build

# The generator tests run against stubs of the Flutter classes; the
# example is the one place the generated code meets the real framework.
- name: Analyze the example
working-directory: ${{ env.EXAMPLE }}
run: flutter analyze --fatal-infos

# A generated file that is new is untracked, which `git diff` cannot
# see; adding it to the index without its content makes it show up.
- name: Fail on stale generated files
run: |
git add --intent-to-add -- '*.g.theme.dart'
git diff --exit-code -- '*.g.theme.dart'
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Workspace resolution lives at the root; the lock file is not published.
.dart_tool/
/pubspec.lock
4 changes: 2 additions & 2 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"clean"
],
"options": {
"cwd": "packages/theme_extensions_builder/test"
"cwd": "packages/theme_extensions_builder"
},
"group": "build",
"presentation": {
Expand All @@ -36,7 +36,7 @@
"--dart-jit-vm-arg=--disable-service-auth-codes"
],
"options": {
"cwd": "packages/theme_extensions_builder/test"
"cwd": "packages/theme_extensions_builder"
},
"dependsOn": "example: run build_runner clean",
"group": "build",
Expand Down
4 changes: 2 additions & 2 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ Or edit `pubspec.yaml` manually:

```yaml
dependencies:
theme_extensions_builder_annotation: ^7.3.0
theme_extensions_builder_annotation: ^7.5.0

dev_dependencies:
build_runner: ^2.13.0
theme_extensions_builder: ^7.3.0
theme_extensions_builder: ^7.5.0
```

Then fetch dependencies:
Expand Down
19 changes: 19 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ dart run build_runner clean
dart run build_runner build
```

## The Build Stops With A Generator Error

The generator checks the annotated class before writing anything. Each message names the class and what to change:

- **`` `X` has no constructor named `_internal`. ``** The `constructor:` option names a constructor that does not exist. Declare it, or point the option at an existing one.
- **`` `X` has no unnamed constructor, which the generated code calls. ``** The class only has named constructors. Add `constructor: 'name'` to the annotation.
- **`` The constructor `X` has no named parameter for the field `y` ``** Every field is passed to the constructor by name. Add `this.y` to the constructor, or mark the field with `@ignore`.
- **`` The constructor `X` requires `y`, which is not among the fields the generated code passes to it ``** The generated code only passes fields, so a required parameter has to be one: a positional parameter, a parameter that is not a field, or a field marked `@ignore` leaves the generated call short of an argument. Make the parameter optional, or declare it as `this.y` without `@ignore`.
- **`` `X<T>` is generic, and the generated mixin cannot be ``** The mixin names the class without type arguments, so a type parameter would be undefined inside it. Remove the type parameters, or write the theme methods by hand.
- **`` The generated mixin `_$X` declares `merge`, so `X` cannot have a field of that name ``** A field is a getter, which cannot override the mixin's method. `copyWith`, `merge` and `lerp` are taken for `@ThemeGen`, `copyWith` and `lerp` for `@ThemeExtensions`. Rename the field.
- **`` `X` does not apply the generated mixin `_$X` ``** The generated methods live in the mixin, so a class without `with _$X` has none of them. Add the clause.
- **`` `X` must extend `ThemeExtension<X>` ``** `@ThemeExtensions` needs a class that extends `ThemeExtension` of itself. Extend it, or use `@ThemeGen` for a plain class.
- **`` `...` is not a valid Dart identifier ``** / **`` `...` is a reserved word ``** `contextAccessorName` is written into the generated code as a getter name. Use an identifier that is not a keyword.
- **`` WidgetStateProperty must have a nullable generic type ``** `WidgetStateProperty.lerp` takes a lerp function with nullable parameters, so the generic has to be nullable: `WidgetStateProperty<Color?>`.

A warning such as `` The `lerp` method of X has an unsupported signature `` does not stop the build. The field type declares a `lerp` or `merge` the generator cannot call, so the field switches over at `t = 0.5` or is overwritten instead. Rename the method or give it a supported signature if it was meant to be used.

A warning such as `` WidgetStateProperty<X?> cannot be interpolated: `X` has no static `X? lerp(X?, X?, double)` `` means the generic of a `WidgetStateProperty` field offers no lerp function for `WidgetStateProperty.lerp` to call, whether it declares no `lerp` at all or one of another shape. The field switches over at `t = 0.5`. Give `X` a static `lerp` that accepts and returns nulls if it was meant to be interpolated.

## Analyzer Errors In Generated Files

Checklist:
Expand Down
7 changes: 7 additions & 0 deletions packages/theme_extensions_builder/.pubignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# The example app is a member of the development workspace: its pubspec.yaml
# says `resolution: workspace`, which does not resolve outside the repository,
# and its platform folders are noise on pub.dev. The sources and the README
# stay, so the Example tab still has something to show.
example/*
!example/lib
!example/README.md
15 changes: 15 additions & 0 deletions packages/theme_extensions_builder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## 7.5.0

- **Breaking**: `hashCode` changes for classes that inherit fields, which now come after the class' own.
- **Changed**: `lerp` keeps the endpoints when one side is null (`t = 0` gives `a`, `t = 1` gives `b`), and `merge` keeps the current value when the incoming one is null.
- **New**: The annotated class is validated before generation. A missing constructor, a field without a named parameter, a required parameter the generated code cannot pass, a generic class, a missing `with _$X`, a field named `copyWith`, `merge` or `lerp`, a `@ThemeExtensions` class not extending `ThemeExtension<Self>`, or an invalid `contextAccessorName` now stop the build with a message naming the class and the fix.
- **Fixed**: Generated code that did not compile for an instance `lerp` on a nullable field or with a nullable result, a non-nullable `lerp`/`merge` parameter on a nullable field, a static call on `Box<int>` instead of `Box`, a method declared on a supertype (the result is cast back), a generic field type (methods resolve through the instantiated type), and a field inherited from a generic superclass (`T` is substituted).
- **Fixed**: An unrelated `lerp` or `merge` no longer fails the build or is mistaken for a supported one; an unusable signature is a build warning naming the field and its fallback.
- **Fixed**: A `WidgetStateProperty` generic has to offer a static `lerp` that accepts nulls, and is reported once when it does not; a non-nullable generic is an error at the field, a nested one falls back; `double` and `Duration` generics are detected by element.
- **Fixed**: Field collection follows Dart's resolution: a narrowed field keeps the narrowed type, mixins win over the superclass chain, `implements` contributes nothing, private and static fields are skipped, and `@ignore` on a redeclaration drops the inherited one. `Duration` is detected by element.
- **Fixed**: A field typed as a `@ThemeGen` class, or a subclass of one, merges the same way on clean and incremental builds; a hand-written `merge` is used as declared.
- **Fixed**: `const` follows the constructor named in `constructor:`; `constructor: ''` selects the unnamed one.
- **Fixed**: Annotations are matched by package as well as by name, so a user class called `ThemeGen` is not taken for one.
- **Fixed**: The example is published without its workspace-only files, so it resolves when copied from pub.dev.
- **Updated**: Analyzer `>=13.1.0 <15.0.0` (the lower bound CI tests against) and Dart SDK `>=3.13.0 <4.0.0`; `collection` and `meta` are no longer dependencies; the `platforms` key is gone from the pubspec.

## 7.4.0

- *Updated*: Analyzer dependency to ">=9.0.0 <14.0.0"
Expand Down
34 changes: 26 additions & 8 deletions packages/theme_extensions_builder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ Or add manually to `pubspec.yaml`:

```yaml
dependencies:
theme_extensions_builder_annotation: ^7.3.0
theme_extensions_builder_annotation: ^7.5.0

dev_dependencies:
build_runner: ^2.13.0
theme_extensions_builder: ^7.4.0
theme_extensions_builder: ^7.5.0
```

## 🚀 Quick Start
Expand Down Expand Up @@ -472,20 +472,38 @@ The example includes ready-to-use components and demonstrates best practices for

## 🔧 Build Configuration

### build.yaml (Optional)

You can customize the build configuration:
The builder takes no options. What `build.yaml` can configure is which files it looks at, which keeps builds fast in a large project:

```yaml
targets:
$default:
builders:
theme_extensions_builder:
enabled: true
options:
# Add custom options here if needed
generate_for:
- lib/theme/**.dart
```

## 🚧 Limitations

- **Types are written by name.** The generated code refers to field types the way the analyzer displays them, without import prefixes. A theme file that imports Flutter `as m` gets a bare `Color` in the generated part, which does not resolve. Import Flutter without a prefix in the files that declare themes.
- **`copyWith` cannot set a nullable field to `null`.** Passing `null` means "keep the current value", as in Flutter's own theme classes.
- **Null handling in `lerp` depends on the type.** `double` and `Duration` treat a missing side as zero, like Flutter's `lerpDouble`. A type with a static `lerp` that accepts nulls, such as `Color`, decides for itself. A type whose `lerp` cannot take a null keeps the nearer side: `t < 0.5` gives the first value, otherwise the second.

## 🚨 Generation Errors

The generator checks the annotated class before it writes anything, and stops the build with a message pointing at the class when:

- the constructor named in `constructor:` does not exist, or there is no unnamed constructor to fall back to;
- a field has no named parameter of the same name in that constructor (mark it `@ignore` if it is not part of the theme);
- that constructor requires a parameter the generated code does not pass: a positional one, one that is not a field, or the parameter of an `@ignore`d field;
- the class is generic, or does not apply the generated `_$ClassName` mixin;
- a field is named after a member the mixin declares (`copyWith`, `merge`, `lerp`);
- a `@ThemeExtensions` class does not extend `ThemeExtension<Self>`;
- `contextAccessorName` is not a valid identifier, or is a reserved word;
- a `WidgetStateProperty` field has a non-nullable generic.

A field type whose `lerp` or `merge` has a signature the generator cannot call is not an error. The build logs a warning naming the field, which then switches over at `t = 0.5` instead of being interpolated, or is overwritten instead of being merged.

## ⚡ Tips and Best Practices

1. **Use descriptive names**: Name your theme extensions clearly (e.g., `ButtonTheme`, `CardTheme`)
Expand Down
3 changes: 1 addition & 2 deletions packages/theme_extensions_builder/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ formatter:

analyzer:
exclude:
- test/theme_gen/goldens/**.dart
- test/theme_extensions/goldens/**.dart
- test/fixtures/goldens/**.dart
7 changes: 5 additions & 2 deletions packages/theme_extensions_builder/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ targets:
builders:
theme_extensions_builder:
generate_for:
- test/theme_gen/**.dart
- test/theme_extensions/**.dart
include:
- test/fixtures/*.dart
exclude:
# These classes are meant to fail generation; see invalid_test.
- test/fixtures/invalid_*.dart

builders:
theme_extensions_builder:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
include: package:pro_lints/common.yaml
include: package:pro_lints/recommended.yaml

formatter:
trailing_commas: preserve
analyzer:
exclude:
- android/**
- ios/**
- macos/**
- linux/**

Loading
Loading