From 434002c06a4f1bf283a8a1a123ecf3588f9565c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=A9=EA=B8=B0=ED=8F=AC?= Date: Sat, 17 Oct 2026 13:54:44 +0900 Subject: [PATCH 1/2] feat: add errorBuilder to the generated SvgGenImage.svg() SvgPicture.errorBuilder is available since flutter_svg 2.0.17, so the parameter is generated only when the resolved flutter_svg version (or version constraint) allows 2.0.17 or later. Co-Authored-By: Claude Opus 5.5 --- examples/example/lib/gen/assets.gen.dart | 2 + .../example_resources/lib/gen/assets.gen.dart | 2 + .../core/lib/generators/assets_generator.dart | 3 + .../integrations/svg_integration.dart | 38 +++++++++++- .../test/assets_gen_integrations_test.dart | 60 +++++++++++++++++++ .../actual_data/assets_assets.gen.dart | 2 + ...assets_assets_deferred_components.gen.dart | 2 + .../assets_assets_directory_path.gen.dart | 2 + ...ctory_path_with_package_parameter.gen.dart | 2 + .../assets_assets_flavored.gen.dart | 2 + .../assets_assets_package_parameter.gen.dart | 2 + ...age_parameter_disable_null_safety.gen.dart | 2 + .../assets_assets_parse_metadata.gen.dart | 2 + .../assets_assets_svg_integrations.gen.dart | 2 + .../actual_data/assets_normal.gen.dart | 2 + .../build_assets_build_assets.gen.dart | 2 + .../build_assets_build_empty.gen.dart | 2 + .../build_assets_build_runner_assets.gen.dart | 2 + 18 files changed, 128 insertions(+), 3 deletions(-) diff --git a/examples/example/lib/gen/assets.gen.dart b/examples/example/lib/gen/assets.gen.dart index 62a550943..ef6a7e76a 100644 --- a/examples/example/lib/gen/assets.gen.dart +++ b/examples/example/lib/gen/assets.gen.dart @@ -359,6 +359,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -395,6 +396,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: colorFilter ?? diff --git a/examples/example_resources/lib/gen/assets.gen.dart b/examples/example_resources/lib/gen/assets.gen.dart index 64f3098e1..a7c1817f4 100644 --- a/examples/example_resources/lib/gen/assets.gen.dart +++ b/examples/example_resources/lib/gen/assets.gen.dart @@ -194,6 +194,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -230,6 +231,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: colorFilter ?? diff --git a/packages/core/lib/generators/assets_generator.dart b/packages/core/lib/generators/assets_generator.dart index f7dfb204a..dd48d91e4 100644 --- a/packages/core/lib/generators/assets_generator.dart +++ b/packages/core/lib/generators/assets_generator.dart @@ -89,6 +89,9 @@ Future generateAssets( SvgIntegration( config.packageParameterLiteral, parseMetadata: config.flutterGen.parseMetadata, + resolvedVersion: config.integrationResolvedVersions[SvgIntegration], + resolvedVersionConstraint: + config.integrationResolvedVersionConstraints[SvgIntegration], ), if (config.flutterGen.integrations.rive) RiveIntegration( diff --git a/packages/core/lib/generators/integrations/svg_integration.dart b/packages/core/lib/generators/integrations/svg_integration.dart index 86919d88a..df5cccc26 100644 --- a/packages/core/lib/generators/integrations/svg_integration.dart +++ b/packages/core/lib/generators/integrations/svg_integration.dart @@ -2,13 +2,45 @@ import 'dart:io'; import 'package:flutter_gen_core/generators/integrations/integration.dart'; import 'package:flutter_gen_core/utils/log.dart'; +import 'package:pub_semver/pub_semver.dart' + show Version, VersionConstraint, VersionRange; import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; class SvgIntegration extends Integration { SvgIntegration( String packageName, { super.parseMetadata, - }) : super(packageName); + Version? resolvedVersion, + VersionConstraint? resolvedVersionConstraint, + }) : supportsErrorBuilder = _resolveErrorBuilderSupport( + resolvedVersion, + resolvedVersionConstraint, + ), + super(packageName); + + /// The minimum flutter_svg version that provides `SvgPicture.errorBuilder`. + static final errorBuilderMinimumSupportVersion = Version(2, 0, 17); + + /// Whether the generated `svg()` exposes `errorBuilder`. + final bool supportsErrorBuilder; + + static bool _resolveErrorBuilderSupport( + Version? resolvedVersion, + VersionConstraint? resolvedVersionConstraint, + ) { + // Resolve by version. + if (resolvedVersion != null) { + return resolvedVersion >= errorBuilderMinimumSupportVersion; + } + // Resolve by version constraint. + if (resolvedVersionConstraint != null) { + return resolvedVersionConstraint.allowsAny( + VersionRange(min: errorBuilderMinimumSupportVersion, includeMin: true), + ); + } + // Use the latest version as the fallback. + return true; + } String get packageExpression => isPackage ? ' = package' : ''; @@ -54,7 +86,7 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''} AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, - String? semanticsLabel, + ${supportsErrorBuilder ? '_svg.SvgErrorWidgetBuilder? errorBuilder,\n ' : ''}String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, _svg.ColorMapper? colorMapper, @@ -90,7 +122,7 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''} alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, - semanticsLabel: semanticsLabel, + ${supportsErrorBuilder ? 'errorBuilder: errorBuilder,\n ' : ''}semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: colorFilter ?? (color == null ? null : ColorFilter.mode(color, colorBlendMode)), clipBehavior: clipBehavior, diff --git a/packages/core/test/assets_gen_integrations_test.dart b/packages/core/test/assets_gen_integrations_test.dart index e07560f33..20312e4ff 100644 --- a/packages/core/test/assets_gen_integrations_test.dart +++ b/packages/core/test/assets_gen_integrations_test.dart @@ -191,6 +191,66 @@ void main() { ); }); + test('SvgIntegration errorBuilder resolution with resolvedVersion', () { + // Test with version < 2.0.17 (should not expose errorBuilder) + final oldIntegration = SvgIntegration( + '', + resolvedVersion: Version(2, 0, 16), + ); + expect(oldIntegration.supportsErrorBuilder, isFalse); + expect(oldIntegration.classOutput.contains('errorBuilder'), isFalse); + + // Test with version >= 2.0.17 (should expose errorBuilder) + final supportedIntegration = SvgIntegration( + '', + resolvedVersion: Version(2, 0, 17), + ); + expect(supportedIntegration.supportsErrorBuilder, isTrue); + expect( + supportedIntegration.classOutput.contains( + '_svg.SvgErrorWidgetBuilder? errorBuilder,', + ), + isTrue, + ); + expect( + supportedIntegration.classOutput.contains( + 'errorBuilder: errorBuilder,', + ), + isTrue, + ); + }); + + test('SvgIntegration errorBuilder resolution with version constraint', () { + // Test with constraint that allows 2.0.17 (should expose errorBuilder) + final allowsSupported = SvgIntegration( + '', + resolvedVersionConstraint: VersionConstraint.parse('^2.0.0'), + ); + expect(allowsSupported.supportsErrorBuilder, isTrue); + + // Test with constraint that excludes 2.0.17 (should not expose errorBuilder) + final excludesSupported = SvgIntegration( + '', + resolvedVersionConstraint: VersionConstraint.parse('>=2.0.0 <2.0.17'), + ); + expect(excludesSupported.supportsErrorBuilder, isFalse); + expect(excludesSupported.classOutput.contains('errorBuilder'), isFalse); + }); + + test('SvgIntegration errorBuilder resolution priority and fallback', () { + // resolvedVersion should take priority over resolvedVersionConstraint + final integration = SvgIntegration( + '', + resolvedVersion: Version(2, 0, 9), + resolvedVersionConstraint: VersionConstraint.parse('^2.0.0'), + ); + expect(integration.supportsErrorBuilder, isFalse); + + // Test with no version information (should expose errorBuilder as fallback) + final fallbackIntegration = SvgIntegration(''); + expect(fallbackIntegration.supportsErrorBuilder, isTrue); + }); + test('Assets with Rive integrations on pubspec.yaml', () async { const pubspec = 'test_resources/pubspec_assets_rive_integrations.yaml'; await expectedAssetsGen(pubspec); diff --git a/packages/core/test_resources/actual_data/assets_assets.gen.dart b/packages/core/test_resources/actual_data/assets_assets.gen.dart index 8adde2e9e..4a28e99cf 100644 --- a/packages/core/test_resources/actual_data/assets_assets.gen.dart +++ b/packages/core/test_resources/actual_data/assets_assets.gen.dart @@ -281,6 +281,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -317,6 +318,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: colorFilter ?? diff --git a/packages/core/test_resources/actual_data/assets_assets_deferred_components.gen.dart b/packages/core/test_resources/actual_data/assets_assets_deferred_components.gen.dart index e6b01d080..2a4b3fc64 100644 --- a/packages/core/test_resources/actual_data/assets_assets_deferred_components.gen.dart +++ b/packages/core/test_resources/actual_data/assets_assets_deferred_components.gen.dart @@ -327,6 +327,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -363,6 +364,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: diff --git a/packages/core/test_resources/actual_data/assets_assets_directory_path.gen.dart b/packages/core/test_resources/actual_data/assets_assets_directory_path.gen.dart index 3a7e669c9..1ae65b05b 100644 --- a/packages/core/test_resources/actual_data/assets_assets_directory_path.gen.dart +++ b/packages/core/test_resources/actual_data/assets_assets_directory_path.gen.dart @@ -188,6 +188,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -224,6 +225,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: diff --git a/packages/core/test_resources/actual_data/assets_assets_directory_path_with_package_parameter.gen.dart b/packages/core/test_resources/actual_data/assets_assets_directory_path_with_package_parameter.gen.dart index bc03f71dd..340dc29ae 100644 --- a/packages/core/test_resources/actual_data/assets_assets_directory_path_with_package_parameter.gen.dart +++ b/packages/core/test_resources/actual_data/assets_assets_directory_path_with_package_parameter.gen.dart @@ -201,6 +201,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -237,6 +238,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: diff --git a/packages/core/test_resources/actual_data/assets_assets_flavored.gen.dart b/packages/core/test_resources/actual_data/assets_assets_flavored.gen.dart index 73fc62631..7c9cf8a3c 100644 --- a/packages/core/test_resources/actual_data/assets_assets_flavored.gen.dart +++ b/packages/core/test_resources/actual_data/assets_assets_flavored.gen.dart @@ -273,6 +273,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -309,6 +310,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: diff --git a/packages/core/test_resources/actual_data/assets_assets_package_parameter.gen.dart b/packages/core/test_resources/actual_data/assets_assets_package_parameter.gen.dart index 256184310..71154fb1f 100644 --- a/packages/core/test_resources/actual_data/assets_assets_package_parameter.gen.dart +++ b/packages/core/test_resources/actual_data/assets_assets_package_parameter.gen.dart @@ -189,6 +189,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -225,6 +226,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: diff --git a/packages/core/test_resources/actual_data/assets_assets_package_parameter_disable_null_safety.gen.dart b/packages/core/test_resources/actual_data/assets_assets_package_parameter_disable_null_safety.gen.dart index 582ea7491..c26af1ce4 100644 --- a/packages/core/test_resources/actual_data/assets_assets_package_parameter_disable_null_safety.gen.dart +++ b/packages/core/test_resources/actual_data/assets_assets_package_parameter_disable_null_safety.gen.dart @@ -177,6 +177,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -213,6 +214,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: diff --git a/packages/core/test_resources/actual_data/assets_assets_parse_metadata.gen.dart b/packages/core/test_resources/actual_data/assets_assets_parse_metadata.gen.dart index 451729d22..e30826f31 100644 --- a/packages/core/test_resources/actual_data/assets_assets_parse_metadata.gen.dart +++ b/packages/core/test_resources/actual_data/assets_assets_parse_metadata.gen.dart @@ -315,6 +315,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -351,6 +352,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: diff --git a/packages/core/test_resources/actual_data/assets_assets_svg_integrations.gen.dart b/packages/core/test_resources/actual_data/assets_assets_svg_integrations.gen.dart index 351ef6d8f..d57ab4dd6 100644 --- a/packages/core/test_resources/actual_data/assets_assets_svg_integrations.gen.dart +++ b/packages/core/test_resources/actual_data/assets_assets_svg_integrations.gen.dart @@ -66,6 +66,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -102,6 +103,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: diff --git a/packages/core/test_resources/actual_data/assets_normal.gen.dart b/packages/core/test_resources/actual_data/assets_normal.gen.dart index 415b35446..9e7050b33 100644 --- a/packages/core/test_resources/actual_data/assets_normal.gen.dart +++ b/packages/core/test_resources/actual_data/assets_normal.gen.dart @@ -236,6 +236,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -272,6 +273,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: diff --git a/packages/core/test_resources/actual_data/build_assets_build_assets.gen.dart b/packages/core/test_resources/actual_data/build_assets_build_assets.gen.dart index 8adde2e9e..4a28e99cf 100644 --- a/packages/core/test_resources/actual_data/build_assets_build_assets.gen.dart +++ b/packages/core/test_resources/actual_data/build_assets_build_assets.gen.dart @@ -281,6 +281,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -317,6 +318,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: colorFilter ?? diff --git a/packages/core/test_resources/actual_data/build_assets_build_empty.gen.dart b/packages/core/test_resources/actual_data/build_assets_build_empty.gen.dart index 8adde2e9e..4a28e99cf 100644 --- a/packages/core/test_resources/actual_data/build_assets_build_empty.gen.dart +++ b/packages/core/test_resources/actual_data/build_assets_build_empty.gen.dart @@ -281,6 +281,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -317,6 +318,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: colorFilter ?? diff --git a/packages/core/test_resources/actual_data/build_assets_build_runner_assets.gen.dart b/packages/core/test_resources/actual_data/build_assets_build_runner_assets.gen.dart index 1e6da27b6..e18af2afa 100644 --- a/packages/core/test_resources/actual_data/build_assets_build_runner_assets.gen.dart +++ b/packages/core/test_resources/actual_data/build_assets_build_runner_assets.gen.dart @@ -274,6 +274,7 @@ class SvgGenImage { AlignmentGeometry alignment = Alignment.center, bool allowDrawingOutsideViewBox = false, WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, String? semanticsLabel, bool excludeFromSemantics = false, _svg.SvgTheme? theme, @@ -310,6 +311,7 @@ class SvgGenImage { alignment: alignment, allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, semanticsLabel: semanticsLabel, excludeFromSemantics: excludeFromSemantics, colorFilter: colorFilter ?? (color == null ? null : ColorFilter.mode(color, colorBlendMode)), From f78bee0dcaed61afb6b644a6c1a268a818015a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=A9=EA=B8=B0=ED=8F=AC?= Date: Sat, 17 Oct 2026 16:28:54 +0900 Subject: [PATCH 2/2] feat: support a custom subclass of SvgGenImage Add `integrations.flutter_svg_class` (name and import) so SVG assets are typed and instantiated with a user-defined subclass of the generated `SvgGenImage`. This lets projects override `svg()` and add their own methods while keeping the generated base class. Co-Authored-By: Claude Opus 5.5 --- README.md | 36 ++++++ .../core/lib/generators/assets_generator.dart | 3 + .../integrations/svg_integration.dart | 24 +++- packages/core/lib/settings/pubspec.dart | 24 ++++ packages/core/lib/settings/pubspec.g.dart | 36 +++++- .../test/assets_gen_integrations_test.dart | 41 ++++++ .../assets_assets_svg_custom_class.gen.dart | 118 ++++++++++++++++++ .../pubspec_assets_svg_custom_class.yaml | 17 +++ 8 files changed, 291 insertions(+), 8 deletions(-) create mode 100644 packages/core/test_resources/actual_data/assets_assets_svg_custom_class.gen.dart create mode 100644 packages/core/test_resources/pubspec_assets_svg_custom_class.yaml diff --git a/README.md b/README.md index f232c15a5..5c4d45943 100644 --- a/README.md +++ b/README.md @@ -514,6 +514,42 @@ Widget build(BuildContext context) { } ``` +To customize the generated SVG API, set `flutter_svg_class` to a class that extends the generated `SvgGenImage`. +SVG assets are then typed and instantiated with that class, so overridden methods such as `svg()` and any added methods are available on every asset. + +```yaml +# pubspec.yaml +flutter_gen: + integrations: + flutter_svg: true + flutter_svg_class: + name: AppSvgGenImage + import: package:my_app/app_svg_gen_image.dart +``` + +```dart +// lib/app_svg_gen_image.dart +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:my_app/gen/assets.gen.dart'; + +class AppSvgGenImage extends SvgGenImage { + const AppSvgGenImage(super.assetName, {super.size, super.flavors}); + + const AppSvgGenImage.vec(super.assetName, {super.size, super.flavors}) + : super.vec(); + + Widget square(double dimension, {Color? color}) { + return svg( + width: dimension, + height: dimension, + colorFilter: + color == null ? null : ColorFilter.mode(color, BlendMode.srcIn), + ); + } +} +``` + **Available Integrations** | Packages | File extension | Setting | Usage | diff --git a/packages/core/lib/generators/assets_generator.dart b/packages/core/lib/generators/assets_generator.dart index dd48d91e4..5d1aa84a5 100644 --- a/packages/core/lib/generators/assets_generator.dart +++ b/packages/core/lib/generators/assets_generator.dart @@ -89,6 +89,9 @@ Future generateAssets( SvgIntegration( config.packageParameterLiteral, parseMetadata: config.flutterGen.parseMetadata, + customClassName: config.flutterGen.integrations.flutterSvgClass?.name, + customClassImport: + config.flutterGen.integrations.flutterSvgClass?.import, resolvedVersion: config.integrationResolvedVersions[SvgIntegration], resolvedVersionConstraint: config.integrationResolvedVersionConstraints[SvgIntegration], diff --git a/packages/core/lib/generators/integrations/svg_integration.dart b/packages/core/lib/generators/integrations/svg_integration.dart index df5cccc26..1db0ef482 100644 --- a/packages/core/lib/generators/integrations/svg_integration.dart +++ b/packages/core/lib/generators/integrations/svg_integration.dart @@ -10,6 +10,8 @@ class SvgIntegration extends Integration { SvgIntegration( String packageName, { super.parseMetadata, + this.customClassName, + this.customClassImport, Version? resolvedVersion, VersionConstraint? resolvedVersionConstraint, }) : supportsErrorBuilder = _resolveErrorBuilderSupport( @@ -18,6 +20,12 @@ class SvgIntegration extends Integration { ), super(packageName); + /// A user-defined subclass of `SvgGenImage` used to instantiate assets. + final String? customClassName; + + /// The import URI of the library that declares [customClassName]. + final String? customClassImport; + /// The minimum flutter_svg version that provides `SvgPicture.errorBuilder`. static final errorBuilderMinimumSupportVersion = Version(2, 0, 17); @@ -45,11 +53,15 @@ class SvgIntegration extends Integration { String get packageExpression => isPackage ? ' = package' : ''; @override - List get requiredImports => const [ - Import('package:flutter/widgets.dart'), - Import('package:flutter/services.dart'), - Import('package:flutter_svg/flutter_svg.dart', alias: '_svg'), - Import('package:vector_graphics/vector_graphics.dart', alias: '_vg'), + List get requiredImports => [ + const Import('package:flutter/widgets.dart'), + const Import('package:flutter/services.dart'), + const Import('package:flutter_svg/flutter_svg.dart', alias: '_svg'), + const Import( + 'package:vector_graphics/vector_graphics.dart', + alias: '_vg', + ), + if (customClassImport case final import?) Import(import), ]; @override @@ -136,7 +148,7 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''} }'''; @override - String get className => 'SvgGenImage'; + String get className => customClassName ?? 'SvgGenImage'; static const vectorCompileTransformer = 'vector_graphics_compiler'; diff --git a/packages/core/lib/settings/pubspec.dart b/packages/core/lib/settings/pubspec.dart index fc5c0ab22..d2d047781 100644 --- a/packages/core/lib/settings/pubspec.dart +++ b/packages/core/lib/settings/pubspec.dart @@ -256,6 +256,7 @@ class FlutterGenIntegrations { required this.flutterSvg, required this.rive, required this.lottie, + this.flutterSvgClass, }); factory FlutterGenIntegrations.fromJson(Map json) => @@ -272,6 +273,29 @@ class FlutterGenIntegrations { @JsonKey(name: 'lottie', required: true) final bool lottie; + + /// A user-defined subclass of `SvgGenImage` used to instantiate SVG assets. + @JsonKey(name: 'flutter_svg_class') + final FlutterGenIntegrationClass? flutterSvgClass; +} + +@JsonSerializable() +class FlutterGenIntegrationClass { + const FlutterGenIntegrationClass({ + required this.name, + required this.import, + }); + + factory FlutterGenIntegrationClass.fromJson(Map json) => + _$FlutterGenIntegrationClassFromJson(json); + + /// The class name, which must extend the generated integration class. + @JsonKey(name: 'name', required: true) + final String name; + + /// The import URI of the library that declares the class. + @JsonKey(name: 'import', required: true) + final String import; } @JsonSerializable() diff --git a/packages/core/lib/settings/pubspec.g.dart b/packages/core/lib/settings/pubspec.g.dart index 3629adef8..5cf805979 100644 --- a/packages/core/lib/settings/pubspec.g.dart +++ b/packages/core/lib/settings/pubspec.g.dart @@ -220,7 +220,13 @@ FlutterGenIntegrations _$FlutterGenIntegrationsFromJson(Map json) => ($checkedConvert) { $checkKeys( json, - allowedKeys: const ['image', 'flutter_svg', 'rive', 'lottie'], + allowedKeys: const [ + 'image', + 'flutter_svg', + 'rive', + 'lottie', + 'flutter_svg_class' + ], requiredKeys: const ['image', 'flutter_svg', 'rive', 'lottie'], ); final val = FlutterGenIntegrations( @@ -228,10 +234,36 @@ FlutterGenIntegrations _$FlutterGenIntegrationsFromJson(Map json) => flutterSvg: $checkedConvert('flutter_svg', (v) => v as bool), rive: $checkedConvert('rive', (v) => v as bool), lottie: $checkedConvert('lottie', (v) => v as bool), + flutterSvgClass: $checkedConvert( + 'flutter_svg_class', + (v) => v == null + ? null + : FlutterGenIntegrationClass.fromJson(v as Map)), + ); + return val; + }, + fieldKeyMap: const { + 'flutterSvg': 'flutter_svg', + 'flutterSvgClass': 'flutter_svg_class' + }, + ); + +FlutterGenIntegrationClass _$FlutterGenIntegrationClassFromJson(Map json) => + $checkedCreate( + 'FlutterGenIntegrationClass', + json, + ($checkedConvert) { + $checkKeys( + json, + allowedKeys: const ['name', 'import'], + requiredKeys: const ['name', 'import'], + ); + final val = FlutterGenIntegrationClass( + name: $checkedConvert('name', (v) => v as String), + import: $checkedConvert('import', (v) => v as String), ); return val; }, - fieldKeyMap: const {'flutterSvg': 'flutter_svg'}, ); FlutterGenElementOutputs _$FlutterGenElementOutputsFromJson(Map json) => diff --git a/packages/core/test/assets_gen_integrations_test.dart b/packages/core/test/assets_gen_integrations_test.dart index 20312e4ff..6bb502f41 100644 --- a/packages/core/test/assets_gen_integrations_test.dart +++ b/packages/core/test/assets_gen_integrations_test.dart @@ -191,6 +191,47 @@ void main() { ); }); + test('Assets with a custom Svg class on pubspec.yaml', () async { + const pubspec = 'test_resources/pubspec_assets_svg_custom_class.yaml'; + await expectedAssetsGen(pubspec); + + final integration = SvgIntegration( + '', + customClassName: 'AppSvgGenImage', + customClassImport: 'package:test/app_svg_gen_image.dart', + ); + // Assets are typed and instantiated with the custom class. + expect(integration.className, 'AppSvgGenImage'); + expect( + integration.classInstantiate( + AssetType( + rootPath: resPath, + path: 'assets/path/dog.svg', + flavors: {}, + transformers: {}, + ), + ), + 'AppSvgGenImage(\'assets/path/dog.svg\')', + ); + expect( + integration.classInstantiate( + AssetType( + rootPath: resPath, + path: 'assets/path/dog.vec', + flavors: {}, + transformers: {}, + ), + ), + 'AppSvgGenImage.vec(\'assets/path/dog.vec\')', + ); + expect( + integration.requiredImports, + contains(const Import('package:test/app_svg_gen_image.dart')), + ); + // The base class is still generated for the custom class to extend. + expect(integration.classOutput.contains('class SvgGenImage {'), isTrue); + }); + test('SvgIntegration errorBuilder resolution with resolvedVersion', () { // Test with version < 2.0.17 (should not expose errorBuilder) final oldIntegration = SvgIntegration( diff --git a/packages/core/test_resources/actual_data/assets_assets_svg_custom_class.gen.dart b/packages/core/test_resources/actual_data/assets_assets_svg_custom_class.gen.dart new file mode 100644 index 000000000..00f8c6e1a --- /dev/null +++ b/packages/core/test_resources/actual_data/assets_assets_svg_custom_class.gen.dart @@ -0,0 +1,118 @@ +// dart format width=80 + +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: deprecated_member_use,directives_ordering,implicit_dynamic_list_literal,unnecessary_import + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart' as _svg; +import 'package:test/app_svg_gen_image.dart'; +import 'package:vector_graphics/vector_graphics.dart' as _vg; + +class $AssetsImagesGen { + const $AssetsImagesGen(); + + /// Directory path: assets/images/icons + $AssetsImagesIconsGen get icons => const $AssetsImagesIconsGen(); +} + +class $AssetsImagesIconsGen { + const $AssetsImagesIconsGen(); + + /// File path: assets/images/icons/fuchsia.svg + AppSvgGenImage get fuchsia => + const AppSvgGenImage('assets/images/icons/fuchsia.svg'); + + /// File path: assets/images/icons/kmm.svg + AppSvgGenImage get kmm => + const AppSvgGenImage.vec('assets/images/icons/kmm.svg'); + + /// List of all assets + List get values => [fuchsia, kmm]; +} + +abstract final class Assets { + static const $AssetsImagesGen images = $AssetsImagesGen(); +} + +class SvgGenImage { + const SvgGenImage(this._assetName, {this.size, this.flavors = const {}}) + : _isVecFormat = false; + + const SvgGenImage.vec(this._assetName, {this.size, this.flavors = const {}}) + : _isVecFormat = true; + + final String _assetName; + final Size? size; + final Set flavors; + final bool _isVecFormat; + + _svg.SvgPicture svg({ + Key? key, + bool matchTextDirection = false, + AssetBundle? bundle, + String? package, + double? width, + double? height, + BoxFit fit = BoxFit.contain, + AlignmentGeometry alignment = Alignment.center, + bool allowDrawingOutsideViewBox = false, + WidgetBuilder? placeholderBuilder, + _svg.SvgErrorWidgetBuilder? errorBuilder, + String? semanticsLabel, + bool excludeFromSemantics = false, + _svg.SvgTheme? theme, + _svg.ColorMapper? colorMapper, + ColorFilter? colorFilter, + Clip clipBehavior = Clip.hardEdge, + @deprecated Color? color, + @deprecated BlendMode colorBlendMode = BlendMode.srcIn, + @deprecated bool cacheColorFilter = false, + }) { + final _svg.BytesLoader loader; + if (_isVecFormat) { + loader = _vg.AssetBytesLoader( + _assetName, + assetBundle: bundle, + packageName: package, + ); + } else { + loader = _svg.SvgAssetLoader( + _assetName, + assetBundle: bundle, + packageName: package, + theme: theme, + colorMapper: colorMapper, + ); + } + return _svg.SvgPicture( + loader, + key: key, + matchTextDirection: matchTextDirection, + width: width, + height: height, + fit: fit, + alignment: alignment, + allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, + placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, + semanticsLabel: semanticsLabel, + excludeFromSemantics: excludeFromSemantics, + colorFilter: + colorFilter ?? + (color == null ? null : ColorFilter.mode(color, colorBlendMode)), + clipBehavior: clipBehavior, + cacheColorFilter: cacheColorFilter, + ); + } + + String get path => _assetName; + + String get keyName => _assetName; +} diff --git a/packages/core/test_resources/pubspec_assets_svg_custom_class.yaml b/packages/core/test_resources/pubspec_assets_svg_custom_class.yaml new file mode 100644 index 000000000..71b0547a3 --- /dev/null +++ b/packages/core/test_resources/pubspec_assets_svg_custom_class.yaml @@ -0,0 +1,17 @@ +name: test + +flutter_gen: + output: lib/gen/ + + integrations: + flutter_svg: true + flutter_svg_class: + name: AppSvgGenImage + import: package:test/app_svg_gen_image.dart + +flutter: + assets: + - assets/images/icons/fuchsia.svg + - path: assets/images/icons/kmm.svg + transformers: + - package: vector_graphics_compiler