Skip to content

Commit

Permalink
✨ Flavored assets (#530)
Browse files Browse the repository at this point in the history
## What does this change?

Resolves #494 🎯

Generated assets would contain the flavor info specified in the `pubspec.yaml`, but only as a constant not involved with actual flavoring.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update
  • Loading branch information
AlexV525 authored Jun 24, 2024
1 parent f678d54 commit 4bf4cdc
Show file tree
Hide file tree
Showing 36 changed files with 1,257 additions and 229 deletions.
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
11 changes: 6 additions & 5 deletions packages/core/lib/generators/integrations/flare_integration.dart
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

0 comments on commit 4bf4cdc

Please sign in to comment.