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

✨ Flavored assets #530

Merged
merged 15 commits into from
Jun 24, 2024
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,18 @@ flutter:

These configurations will generate **`assets.gen.dart`** under the **`lib/gen/`** directory by default.

#### Flavored assets

Flutter supports
[Conditionally bundling assets based on flavor](https://docs.flutter.dev/deployment/flavors#conditionally-bundling-assets-based-on-flavor).
Assets are only available with flavors if specified.
`flutter_gen` will generate the specified `flavors` for assets regardless the current flavor.
The `flavors` field accessible though `.flavors`, for example:

```dart
print(MyAssets.images.chip4.flavors); // -> {'extern'}
```

#### Excluding generating for assets

You can specify `flutter_gen > assets > exclude` using `Glob` patterns to exclude particular assets.
Expand Down
74 changes: 60 additions & 14 deletions examples/example/lib/gen/assets.gen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class $AssetsImagesGen {
/// File path: assets/images/chip2.jpg
AssetGenImage get chip2 => const AssetGenImage('assets/images/chip2.jpg');

/// Directory path: assets/images/chip3
$AssetsImagesChip3Gen get chip3 => const $AssetsImagesChip3Gen();

/// Directory path: assets/images/chip4
$AssetsImagesChip4Gen get chip4 => const $AssetsImagesChip4Gen();

Expand Down Expand Up @@ -142,12 +145,25 @@ class $AssetsUnknownGen {
List<String> get values => [changelog, readme, unknownMimeType];
}

class $AssetsImagesChip3Gen {
const $AssetsImagesChip3Gen();

/// File path: assets/images/chip3/chip3.jpg
AssetGenImage get chip3 =>
const AssetGenImage('assets/images/chip3/chip3.jpg');

/// List of all assets
List<AssetGenImage> get values => [chip3];
}

class $AssetsImagesChip4Gen {
const $AssetsImagesChip4Gen();

/// File path: assets/images/chip4/chip4.jpg
AssetGenImage get chip4 =>
const AssetGenImage('assets/images/chip4/chip4.jpg');
AssetGenImage get chip4 => const AssetGenImage(
'assets/images/chip4/chip4.jpg',
flavors: {'extern'},
);

/// List of all assets
List<AssetGenImage> get values => [chip4];
Expand Down Expand Up @@ -202,11 +218,16 @@ class MyAssets {
}

class AssetGenImage {
const AssetGenImage(this._assetName, {this.size = null});
const AssetGenImage(
this._assetName, {
this.size,
this.flavors = const {},
});

final String _assetName;

final Size? size;
final Set<String> flavors;

Image image({
Key? key,
Expand Down Expand Up @@ -280,17 +301,19 @@ class AssetGenImage {
class SvgGenImage {
const SvgGenImage(
this._assetName, {
this.size = null,
this.size,
this.flavors = const {},
}) : _isVecFormat = false;

const SvgGenImage.vec(
this._assetName, {
this.size = null,
this.size,
this.flavors = const {},
}) : _isVecFormat = true;

final String _assetName;

final Size? size;
final Set<String> flavors;
final bool _isVecFormat;

SvgPicture svg({
Expand All @@ -313,12 +336,23 @@ class SvgGenImage {
@deprecated BlendMode colorBlendMode = BlendMode.srcIn,
@deprecated bool cacheColorFilter = false,
}) {
final BytesLoader loader;
if (_isVecFormat) {
loader = AssetBytesLoader(
_assetName,
assetBundle: bundle,
packageName: package,
);
} else {
loader = SvgAssetLoader(
_assetName,
assetBundle: bundle,
packageName: package,
theme: theme,
);
}
return SvgPicture(
_isVecFormat
? AssetBytesLoader(_assetName,
assetBundle: bundle, packageName: package)
: SvgAssetLoader(_assetName,
assetBundle: bundle, packageName: package, theme: theme),
loader,
key: key,
matchTextDirection: matchTextDirection,
width: width,
Expand All @@ -342,9 +376,13 @@ class SvgGenImage {
}

class FlareGenImage {
const FlareGenImage(this._assetName);
const FlareGenImage(
this._assetName, {
this.flavors = const {},
});

final String _assetName;
final Set<String> flavors;

FlareActor flare({
String? boundsNode,
Expand Down Expand Up @@ -385,9 +423,13 @@ class FlareGenImage {
}

class RiveGenImage {
const RiveGenImage(this._assetName);
const RiveGenImage(
this._assetName, {
this.flavors = const {},
});

final String _assetName;
final Set<String> flavors;

RiveAnimation rive({
String? artboard,
Expand Down Expand Up @@ -422,9 +464,13 @@ class RiveGenImage {
}

class LottieGenImage {
const LottieGenImage(this._assetName);
const LottieGenImage(
this._assetName, {
this.flavors = const {},
});

final String _assetName;
final Set<String> flavors;

LottieBuilder lottie({
Animation<double>? controller,
Expand Down
6 changes: 4 additions & 2 deletions examples/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ flutter_gen:
style: dot-delimiter

exclude:
- assets/images/chip3/chip3.jpg
- assets-extern/*
- pictures/chip5.jpg

Expand All @@ -76,7 +75,6 @@ flutter:
- assets/images/
- assets/images/chip3/chip3.jpg
- assets/images/chip3/chip3.jpg # duplicated
- assets/images/chip4/
- assets/images/icons/fuchsia.svg
- assets/images/icons/kmm.svg
- assets/images/icons/paint.svg
Expand All @@ -91,6 +89,10 @@ flutter:
- assets/mix/
- assets-extern/
- pictures/chip5.jpg

- path: assets/images/chip4/chip4.jpg
flavors:
- extern
fonts:
- family: Raleway
fonts:
Expand Down
95 changes: 71 additions & 24 deletions packages/core/lib/generators/assets_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import 'package:flutter_gen_core/generators/integrations/rive_integration.dart';
import 'package:flutter_gen_core/generators/integrations/svg_integration.dart';
import 'package:flutter_gen_core/settings/asset_type.dart';
import 'package:flutter_gen_core/settings/config.dart';
import 'package:flutter_gen_core/settings/flavored_asset.dart';
import 'package:flutter_gen_core/settings/pubspec.dart';
import 'package:flutter_gen_core/utils/error.dart';
import 'package:flutter_gen_core/utils/string.dart';
import 'package:glob/glob.dart';
import 'package:path/path.dart';
import 'package:yaml/yaml.dart';

class AssetsGenConfig {
AssetsGenConfig._(
Expand All @@ -41,7 +43,7 @@ class AssetsGenConfig {
final String rootPath;
final String _packageName;
final FlutterGen flutterGen;
final List<String> assets;
final List<Object> assets;
final List<Glob> exclude;

String get packageParameterLiteral =>
Expand Down Expand Up @@ -194,51 +196,89 @@ String? generatePackageNameForConfig(AssetsGenConfig config) {
}
}

/// Returns a list of all releative path assets that are to be considered.
List<String> _getAssetRelativePathList(
/// Returns a list of all relative path assets that are to be considered.
List<FlavoredAsset> _getAssetRelativePathList(
/// The absolute root path of the assets directory.
String rootPath,

/// List of assets as provided the `flutter`.`assets` section in the pubspec.yaml.
List<String> assets,
/// List of assets as provided the `flutter -> assets`
/// section in the pubspec.yaml.
List<Object> assets,

/// List of globs as provided the `flutter_gen`.`assets`.`exclude` section in the pubspec.yaml.
/// List of globs as provided the `flutter_gen -> assets -> exclude`
/// section in the pubspec.yaml.
List<Glob> excludes,
) {
final assetRelativePathList = <String>[];
for (final assetName in assets) {
final assetAbsolutePath = join(rootPath, assetName);
// Normalize.
final normalizedAssets = <Object>{...assets.whereType<String>()};
final normalizingMap = <String, Set<String>>{};
// Resolve flavored assets.
for (final map in assets.whereType<YamlMap>()) {
final path = (map['path'] as String).trim();
final flavors =
(map['flavors'] as YamlList?)?.toSet().cast<String>() ?? <String>{};
if (normalizingMap.containsKey(path)) {
// https://github.com/flutter/flutter/blob/5187cab7bdd434ca74abb45895d17e9fa553678a/packages/flutter_tools/lib/src/asset.dart#L1137-L1139
throw StateError(
'Multiple assets entries include the file "$path", '
'but they specify different lists of flavors.',
);
}
normalizingMap[path] = flavors;
}
for (final entry in normalizingMap.entries) {
normalizedAssets.add(
YamlMap.wrap({'path': entry.key, 'flavors': entry.value}),
);
}

final assetRelativePathList = <FlavoredAsset>[];
for (final asset in normalizedAssets) {
final FlavoredAsset tempAsset;
if (asset is YamlMap) {
tempAsset = FlavoredAsset(path: asset['path'], flavors: asset['flavors']);
} else {
tempAsset = FlavoredAsset(path: (asset as String).trim());
}
final assetAbsolutePath = join(rootPath, tempAsset.path);
if (FileSystemEntity.isDirectorySync(assetAbsolutePath)) {
assetRelativePathList.addAll(Directory(assetAbsolutePath)
.listSync()
.whereType<File>()
.map((e) => relative(e.path, from: rootPath))
.map(
(e) => tempAsset.copyWith(path: relative(e.path, from: rootPath)),
)
.toList());
} else if (FileSystemEntity.isFileSync(assetAbsolutePath)) {
assetRelativePathList.add(relative(assetAbsolutePath, from: rootPath));
assetRelativePathList.add(
tempAsset.copyWith(path: relative(assetAbsolutePath, from: rootPath)),
);
}
}

if (excludes.isEmpty) {
return assetRelativePathList;
}

return assetRelativePathList
.where((file) => !excludes.any((exclude) => exclude.matches(file)))
.where((asset) => !excludes.any((exclude) => exclude.matches(asset.path)))
.toList();
}

AssetType _constructAssetTree(
List<String> assetRelativePathList, String rootPath) {
List<FlavoredAsset> assetRelativePathList,
String rootPath,
) {
// Relative path is the key
final assetTypeMap = <String, AssetType>{
'.': AssetType(rootPath: rootPath, path: '.'),
'.': AssetType(rootPath: rootPath, path: '.', flavors: {}),
};
for (final assetPath in assetRelativePathList) {
var path = assetPath;
for (final asset in assetRelativePathList) {
String path = asset.path;
while (path != '.') {
assetTypeMap.putIfAbsent(
path, () => AssetType(rootPath: rootPath, path: path));
path,
() => AssetType(rootPath: rootPath, path: path, flavors: asset.flavors),
);
path = dirname(path);
}
}
Expand Down Expand Up @@ -320,7 +360,8 @@ String _dotDelimiterStyleDefinition(
final assetsStaticStatements = <_Statement>[];

final assetTypeQueue = ListQueue<AssetType>.from(
_constructAssetTree(assetRelativePathList, rootPath).children);
_constructAssetTree(assetRelativePathList, rootPath).children,
);

while (assetTypeQueue.isNotEmpty) {
final assetType = assetTypeQueue.removeFirst();
Expand Down Expand Up @@ -428,14 +469,20 @@ String _flatStyleDefinition(
List<Integration> integrations,
String Function(String) style,
) {
final statements = _getAssetRelativePathList(
final List<FlavoredAsset> paths = _getAssetRelativePathList(
config.rootPath,
config.assets,
config.exclude,
)
.distinct()
.sorted()
.map((assetPath) => AssetType(rootPath: config.rootPath, path: assetPath))
);
paths.sort(((a, b) => a.path.compareTo(b.path)));
final statements = paths
.map(
(assetPath) => AssetType(
rootPath: config.rootPath,
path: assetPath.path,
flavors: assetPath.flavors,
),
)
.mapToUniqueAssetType(style)
.map(
(e) => _createAssetTypeStatement(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ class FlareIntegration extends Integration {
String get classOutput => _classDefinition;

String get _classDefinition => '''class FlareGenImage {
const FlareGenImage(this._assetName);
const FlareGenImage(
this._assetName, {
this.flavors = const {},
});

final String _assetName;
final Set<String> flavors;

${isPackage ? "\n static const String package = '$packageName';" : ''}

FlareActor flare({
Expand Down Expand Up @@ -63,10 +68,6 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''}
@override
String get className => 'FlareGenImage';

@override
String classInstantiate(AssetType asset) =>
'FlareGenImage(\'${asset.posixStylePath}\')';

@override
bool isSupport(AssetType asset) => asset.extension == '.flr';

Expand Down
Loading
Loading