diff --git a/dev/benchmarks/microbenchmarks/lib/foundation/decode_and_parse_asset_manifest.dart b/dev/benchmarks/microbenchmarks/lib/foundation/decode_and_parse_asset_manifest.dart index d6b0f7c6732..b64c1532ce2 100644 --- a/dev/benchmarks/microbenchmarks/lib/foundation/decode_and_parse_asset_manifest.dart +++ b/dev/benchmarks/microbenchmarks/lib/foundation/decode_and_parse_asset_manifest.dart @@ -3,9 +3,9 @@ // found in the LICENSE file. import 'dart:convert'; -import 'dart:typed_data'; -import 'package:flutter/services.dart' show PlatformAssetBundle, StandardMessageCodec; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart' show PlatformAssetBundle; import 'package:flutter/widgets.dart'; import '../common.dart'; @@ -18,14 +18,16 @@ void main() async { final BenchmarkResultPrinter printer = BenchmarkResultPrinter(); WidgetsFlutterBinding.ensureInitialized(); final Stopwatch watch = Stopwatch(); + final PlatformAssetBundle bundle = PlatformAssetBundle(); - final ByteData assetManifest = await loadAssetManifest(); - + final ByteData assetManifestBytes = await bundle.load('money_asset_manifest.json'); watch.start(); for (int i = 0; i < _kNumIterations; i++) { - // This is effectively a test. + bundle.clear(); + final String json = utf8.decode(assetManifestBytes.buffer.asUint8List()); + // This is a test, so we don't need to worry about this rule. // ignore: invalid_use_of_visible_for_testing_member - AssetImage.parseAssetManifest(assetManifest); + await AssetImage.manifestParser(json); } watch.stop(); @@ -38,49 +40,3 @@ void main() async { printer.printToStdout(); } - -final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$'); - -Future loadAssetManifest() async { - double parseScale(String key) { - final Uri assetUri = Uri.parse(key); - String directoryPath = ''; - if (assetUri.pathSegments.length > 1) { - directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2]; - } - final Match? match = _extractRatioRegExp.firstMatch(directoryPath); - if (match != null && match.groupCount > 0) { - return double.parse(match.group(1)!); - } - return 1.0; - } - - final Map result = {}; - final PlatformAssetBundle bundle = PlatformAssetBundle(); - - // For the benchmark, we use the older JSON format and then convert it to the modern binary format. - final ByteData jsonAssetManifestBytes = await bundle.load('money_asset_manifest.json'); - final String jsonAssetManifest = utf8.decode(jsonAssetManifestBytes.buffer.asUint8List()); - - final Map assetManifest = json.decode(jsonAssetManifest) as Map; - - for (final MapEntry manifestEntry in assetManifest.entries) { - final List resultVariants = []; - final List entries = (manifestEntry.value as List).cast(); - for (final String variant in entries) { - if (variant == manifestEntry.key) { - // With the newer binary format, don't include the main asset in it's - // list of variants. This reduces parsing time at runtime. - continue; - } - final Map resultVariant = {}; - final double variantDevicePixelRatio = parseScale(variant); - resultVariant['asset'] = variant; - resultVariant['dpr'] = variantDevicePixelRatio; - resultVariants.add(resultVariant); - } - result[manifestEntry.key] = resultVariants; - } - - return const StandardMessageCodec().encodeMessage(result)!; -} diff --git a/dev/integration_tests/flutter_gallery/test/example_code_parser_test.dart b/dev/integration_tests/flutter_gallery/test/example_code_parser_test.dart index a1baa460ed3..94276fb38e1 100644 --- a/dev/integration_tests/flutter_gallery/test/example_code_parser_test.dart +++ b/dev/integration_tests/flutter_gallery/test/example_code_parser_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:flutter/services.dart'; import 'package:flutter_gallery/gallery/example_code_parser.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -60,9 +58,4 @@ class TestAssetBundle extends AssetBundle { @override String toString() => '$runtimeType@$hashCode()'; - - @override - Future loadStructuredBinaryData(String key, FutureOr Function(ByteData data) parser) async { - return parser(await load(key)); - } } diff --git a/packages/flutter/lib/src/painting/image_resolution.dart b/packages/flutter/lib/src/painting/image_resolution.dart index f978dff4c8d..1e427742bf9 100644 --- a/packages/flutter/lib/src/painting/image_resolution.dart +++ b/packages/flutter/lib/src/painting/image_resolution.dart @@ -11,8 +11,7 @@ import 'package:flutter/services.dart'; import 'image_provider.dart'; -const String _kLegacyAssetManifestFilename = 'AssetManifest.json'; -const String _kAssetManifestFilename = 'AssetManifest.bin'; +const String _kAssetManifestFileName = 'AssetManifest.json'; /// A screen with a device-pixel ratio strictly less than this value is /// considered a low-resolution screen (typically entry-level to mid-range @@ -285,45 +284,18 @@ class AssetImage extends AssetBundleImageProvider { Completer? completer; Future? result; - Future<_AssetManifest> loadJsonAssetManifest() { - Future<_AssetManifest> parseJson(String data) { - final _AssetManifest parsed = _LegacyAssetManifest.fromJsonString(data); - return SynchronousFuture<_AssetManifest>(parsed); - } - return chosenBundle.loadStructuredData(_kLegacyAssetManifestFilename, parseJson); - } - - // TODO(andrewkolos): Once google3 and google-fonts-flutter are migrated - // away from using AssetManifest.json, remove all references to it. - // See https://github.com/flutter/flutter/issues/114913. - Future<_AssetManifest>? manifest; - - // Since AssetBundle load calls can be synchronous (e.g. in the case of tests), - // it is not sufficient to only use catchError/onError or the onError parameter - // of Future.then--we also have to use a synchronous try/catch. Once google3 - // tooling starts producing AssetManifest.bin, this block can be removed. - try { - manifest = chosenBundle.loadStructuredBinaryData(_kAssetManifestFilename, _AssetManifestBin.fromStandardMessageCodecMessage); - } catch (error) { - manifest = loadJsonAssetManifest(); - } - - manifest - // To understand why we use this no-op `then` instead of `catchError`/`onError`, - // see https://github.com/flutter/flutter/issues/115601 - .then((_AssetManifest manifest) => manifest, - onError: (Object? error, StackTrace? stack) => loadJsonAssetManifest()) - .then((_AssetManifest manifest) { - final List<_AssetVariant> candidateVariants = manifest.getVariants(keyName); - final _AssetVariant chosenVariant = _chooseVariant( + chosenBundle.loadStructuredData>?>(_kAssetManifestFileName, manifestParser).then( + (Map>? manifest) { + final String chosenName = _chooseVariant( keyName, configuration, - candidateVariants, - ); + manifest == null ? null : manifest[keyName], + )!; + final double chosenScale = _parseScale(chosenName); final AssetBundleImageKey key = AssetBundleImageKey( bundle: chosenBundle, - name: chosenVariant.asset, - scale: chosenVariant.devicePixelRatio, + name: chosenName, + scale: chosenScale, ); if (completer != null) { // We already returned from this function, which means we are in the @@ -337,15 +309,14 @@ class AssetImage extends AssetBundleImageProvider { // ourselves. result = SynchronousFuture(key); } - }) - .onError((Object error, StackTrace stack) { - // We had an error. (This guarantees we weren't called synchronously.) - // Forward the error to the caller. - assert(completer != null); - assert(result == null); - completer!.completeError(error, stack); - }); - + }, + ).catchError((Object error, StackTrace stack) { + // We had an error. (This guarantees we weren't called synchronously.) + // Forward the error to the caller. + assert(completer != null); + assert(result == null); + completer!.completeError(error, stack); + }); if (result != null) { // The code above ran synchronously, and came up with an answer. // Return the SynchronousFuture that we created above. @@ -357,29 +328,35 @@ class AssetImage extends AssetBundleImageProvider { return completer.future; } - /// Parses the asset manifest's file contents into it's Dart representation. + /// Parses the asset manifest string into a strongly-typed map. @visibleForTesting - // Return type is set to Object?, because the specific type is private. - static Object? parseAssetManifest(ByteData bytes) { - return _AssetManifestBin.fromStandardMessageCodecMessage(bytes); + static Future>?> manifestParser(String? jsonData) { + if (jsonData == null) { + return SynchronousFuture>?>(null); + } + // TODO(ianh): JSON decoding really shouldn't be on the main thread. + final Map parsedJson = json.decode(jsonData) as Map; + final Iterable keys = parsedJson.keys; + final Map> parsedManifest = > { + for (final String key in keys) key: List.from(parsedJson[key] as List), + }; + // TODO(ianh): convert that data structure to the right types. + return SynchronousFuture>?>(parsedManifest); } - _AssetVariant _chooseVariant(String mainAssetKey, ImageConfiguration config, List<_AssetVariant> candidateVariants) { - final _AssetVariant mainAsset = _AssetVariant(asset: mainAssetKey, - devicePixelRatio: _naturalResolution); - if (config.devicePixelRatio == null || candidateVariants.isEmpty) { - return mainAsset; + String? _chooseVariant(String main, ImageConfiguration config, List? candidates) { + if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) { + return main; } - final SplayTreeMap candidatesByDevicePixelRatio = - SplayTreeMap(); - for (final _AssetVariant candidate in candidateVariants) { - candidatesByDevicePixelRatio[candidate.devicePixelRatio] = candidate; + // TODO(ianh): Consider moving this parsing logic into _manifestParser. + final SplayTreeMap mapping = SplayTreeMap(); + for (final String candidate in candidates) { + mapping[_parseScale(candidate)] = candidate; } - candidatesByDevicePixelRatio.putIfAbsent(_naturalResolution, () => mainAsset); // TODO(ianh): implement support for config.locale, config.textDirection, // config.size, config.platform (then document this over in the Image.asset // docs) - return _findBestVariant(candidatesByDevicePixelRatio, config.devicePixelRatio!); + return _findBestVariant(mapping, config.devicePixelRatio!); } // Returns the "best" asset variant amongst the available `candidates`. @@ -394,17 +371,17 @@ class AssetImage extends AssetBundleImageProvider { // lowest key higher than `value`. // - If the screen has high device pixel ratio, choose the variant with the // key nearest to `value`. - _AssetVariant _findBestVariant(SplayTreeMap candidatesByDpr, double value) { - if (candidatesByDpr.containsKey(value)) { - return candidatesByDpr[value]!; + String? _findBestVariant(SplayTreeMap candidates, double value) { + if (candidates.containsKey(value)) { + return candidates[value]!; } - final double? lower = candidatesByDpr.lastKeyBefore(value); - final double? upper = candidatesByDpr.firstKeyAfter(value); + final double? lower = candidates.lastKeyBefore(value); + final double? upper = candidates.firstKeyAfter(value); if (lower == null) { - return candidatesByDpr[upper]!; + return candidates[upper]; } if (upper == null) { - return candidatesByDpr[lower]!; + return candidates[lower]; } // On screens with low device-pixel ratios the artifacts from upscaling @@ -412,12 +389,32 @@ class AssetImage extends AssetBundleImageProvider { // ratios because the physical pixels are larger. Choose the higher // resolution image in that case instead of the nearest one. if (value < _kLowDprLimit || value > (lower + upper) / 2) { - return candidatesByDpr[upper]!; + return candidates[upper]; } else { - return candidatesByDpr[lower]!; + return candidates[lower]; } } + static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$'); + + double _parseScale(String key) { + if (key == assetName) { + return _naturalResolution; + } + + final Uri assetUri = Uri.parse(key); + String directoryPath = ''; + if (assetUri.pathSegments.length > 1) { + directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2]; + } + + final Match? match = _extractRatioRegExp.firstMatch(directoryPath); + if (match != null && match.groupCount > 0) { + return double.parse(match.group(1)!); + } + return _naturalResolution; // i.e. default to 1.0x + } + @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { @@ -434,120 +431,3 @@ class AssetImage extends AssetBundleImageProvider { @override String toString() => '${objectRuntimeType(this, 'AssetImage')}(bundle: $bundle, name: "$keyName")'; } - -/// Centralizes parsing and typecasting of the contents of the asset manifest file, -/// which is generated by the flutter tool at build time. -abstract class _AssetManifest { - List<_AssetVariant> getVariants(String key); -} - -/// Parses the binary asset manifest into a data structure that's easier to work with. -/// -/// The asset manifest is a map of asset files to a list of objects containing -/// information about variants of that asset. -/// -/// The entries with each variant object are: -/// - "asset": the location of this variant to load it from. -/// - "dpr": The device-pixel-ratio that the asset is best-suited for. -/// -/// New fields could be added to this object schema to support new asset variation -/// features, such as themes, locale/region support, reading directions, and so on. -class _AssetManifestBin implements _AssetManifest { - _AssetManifestBin(Map standardMessageData): _data = standardMessageData; - - factory _AssetManifestBin.fromStandardMessageCodecMessage(ByteData message) { - final Object? data = const StandardMessageCodec().decodeMessage(message); - return _AssetManifestBin(data! as Map); - } - - final Map _data; - final Map> _typeCastedData = >{}; - - @override - List<_AssetVariant> getVariants(String key) { - // We lazily delay typecasting to prevent a performance hiccup when parsing - // large asset manifests. - if (!_typeCastedData.containsKey(key)) { - _typeCastedData[key] = ((_data[key] ?? []) as List) - .cast>() - .map(_AssetVariant.fromManifestData) - .toList(); - } - return _typeCastedData[key]!; - } -} - -class _LegacyAssetManifest implements _AssetManifest { - _LegacyAssetManifest({ - required this.manifest, - }); - - factory _LegacyAssetManifest.fromJsonString(String jsonString) { - List<_AssetVariant> adaptLegacyVariantList(String mainAsset, List variants) { - return variants - .map((String variant) => - _AssetVariant(asset: variant, devicePixelRatio: _parseScale(mainAsset, variant))) - .toList(); - } - - if (jsonString == null) { - return _LegacyAssetManifest(manifest: >{}); - } - final Map parsedJson = json.decode(jsonString) as Map; - final Iterable keys = parsedJson.keys; - final Map> parsedManifest = > { - for (final String key in keys) key: List.from(parsedJson[key]! as List), - }; - final Map> manifestWithParsedVariants = - parsedManifest.map((String asset, List variants) => - MapEntry>(asset, adaptLegacyVariantList(asset, variants))); - - return _LegacyAssetManifest(manifest: manifestWithParsedVariants); - } - - final Map> manifest; - - static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$'); - static const double _naturalResolution = 1.0; - - @override - List<_AssetVariant> getVariants(String key) { - return manifest[key] ?? const <_AssetVariant>[]; - } - - static double _parseScale(String mainAsset, String variant) { - // The legacy asset manifest includes the main asset within its variant list. - if (mainAsset == variant) { - return _naturalResolution; - } - - final Uri assetUri = Uri.parse(variant); - String directoryPath = ''; - if (assetUri.pathSegments.length > 1) { - directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2]; - } - - final Match? match = _extractRatioRegExp.firstMatch(directoryPath); - if (match != null && match.groupCount > 0) { - return double.parse(match.group(1)!); - } - - return _naturalResolution; // i.e. default to 1.0x - } -} - -class _AssetVariant { - _AssetVariant({ - required this.asset, - required this.devicePixelRatio, - }); - - factory _AssetVariant.fromManifestData(Object data) { - final Map asStructuredData = data as Map; - return _AssetVariant(asset: asStructuredData['asset']! as String, - devicePixelRatio: asStructuredData['dpr']! as double); - } - - final double devicePixelRatio; - final String asset; -} diff --git a/packages/flutter/lib/src/services/asset_bundle.dart b/packages/flutter/lib/src/services/asset_bundle.dart index ba971e20aea..776035aa687 100644 --- a/packages/flutter/lib/src/services/asset_bundle.dart +++ b/packages/flutter/lib/src/services/asset_bundle.dart @@ -96,25 +96,12 @@ abstract class AssetBundle { } /// Retrieve a string from the asset bundle, parse it with the given function, - /// and return that function's result. + /// and return the function's result. /// /// Implementations may cache the result, so a particular key should only be /// used with one parser for the lifetime of the asset bundle. Future loadStructuredData(String key, Future Function(String value) parser); - /// Retrieve [ByteData] from the asset bundle, parse it with the given function, - /// and return that function's result. - /// - /// Implementations may cache the result, so a particular key should only be - /// used with one parser for the lifetime of the asset bundle. - Future loadStructuredBinaryData(String key, FutureOr Function(ByteData data) parser) async { - final ByteData data = await load(key); - if (data == null) { - throw FlutterError('Unable to load asset: $key'); - } - return parser(data); - } - /// If this is a caching asset bundle, and the given key describes a cached /// asset, then evict the asset from the cache so that the next time it is /// loaded, the cache will be reread from the asset bundle. @@ -169,18 +156,6 @@ class NetworkAssetBundle extends AssetBundle { return parser(await loadString(key)); } - /// Retrieve [ByteData] from the asset bundle, parse it with the given function, - /// and return the function's result. - /// - /// The result is not cached. The parser is run each time the resource is - /// fetched. - @override - Future loadStructuredBinaryData(String key, FutureOr Function(ByteData data) parser) async { - assert(key != null); - assert(parser != null); - return parser(await load(key)); - } - // TODO(ianh): Once the underlying network logic learns about caching, we // should implement evict(). @@ -200,7 +175,6 @@ abstract class CachingAssetBundle extends AssetBundle { // TODO(ianh): Replace this with an intelligent cache, see https://github.com/flutter/flutter/issues/3568 final Map> _stringCache = >{}; final Map> _structuredDataCache = >{}; - final Map> _structuredBinaryDataCache = >{}; @override Future loadString(String key, { bool cache = true }) { @@ -251,69 +225,16 @@ abstract class CachingAssetBundle extends AssetBundle { return completer.future; } - /// Retrieve bytedata from the asset bundle, parse it with the given function, - /// and return the function's result. - /// - /// The result of parsing the bytedata is cached (the bytedata itself is not). - /// For any given `key`, the `parser` is only run the first time. - /// - /// Once the value has been parsed, the future returned by this function for - /// subsequent calls will be a [SynchronousFuture], which resolves its - /// callback synchronously. - @override - Future loadStructuredBinaryData(String key, FutureOr Function(ByteData data) parser) { - assert(key != null); - assert(parser != null); - - if (_structuredBinaryDataCache.containsKey(key)) { - return _structuredBinaryDataCache[key]! as Future; - } - - // load can return a SynchronousFuture in certain cases, like in the - // flutter_test framework. So, we need to support both async and sync flows. - Completer? completer; // For async flow. - SynchronousFuture? result; // For sync flow. - - load(key) - .then(parser) - .then((T value) { - result = SynchronousFuture(value); - if (completer != null) { - // The load and parse operation ran asynchronously. We already returned - // from the loadStructuredBinaryData function and therefore the caller - // was given the future of the completer. - completer.complete(value); - } - }, onError: (Object err, StackTrace? stack) { - completer!.completeError(err, stack); - }); - - if (result != null) { - // The above code ran synchronously. We can synchronously return the result. - _structuredBinaryDataCache[key] = result!; - return result!; - } - - // Since the above code is being run asynchronously and thus hasn't run its - // `then` handler yet, we'll return a completer that will be completed - // when the handler does run. - completer = Completer(); - _structuredBinaryDataCache[key] = completer.future; - return completer.future; - } - @override void evict(String key) { _stringCache.remove(key); _structuredDataCache.remove(key); - _structuredBinaryDataCache.remove(key); } @override void clear() { _stringCache.clear(); _structuredDataCache.clear(); - _structuredBinaryDataCache.clear(); } @override @@ -355,7 +276,7 @@ class PlatformAssetBundle extends CachingAssetBundle { bool debugUsePlatformChannel = false; assert(() { // dart:io is safe to use here since we early return for web - // above. If that code is changed, this needs to be guarded on + // above. If that code is changed, this needs to be gaurded on // web presence. Override how assets are loaded in tests so that // the old loader behavior that allows tests to load assets from // the current package using the package prefix. diff --git a/packages/flutter/test/painting/image_resolution_test.dart b/packages/flutter/test/painting/image_resolution_test.dart index 962cf9d83ee..8e04f2aada4 100644 --- a/packages/flutter/test/painting/image_resolution_test.dart +++ b/packages/flutter/test/painting/image_resolution_test.dart @@ -13,14 +13,18 @@ import 'package:flutter_test/flutter_test.dart'; class TestAssetBundle extends CachingAssetBundle { TestAssetBundle(this._assetBundleMap); - final Map>> _assetBundleMap; + final Map> _assetBundleMap; Map loadCallCount = {}; + String get _assetBundleContents { + return json.encode(_assetBundleMap); + } + @override Future load(String key) async { - if (key == 'AssetManifest.bin') { - return const StandardMessageCodec().encodeMessage(_assetBundleMap)!; + if (key == 'AssetManifest.json') { + return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(_assetBundleContents)).buffer); } loadCallCount[key] = loadCallCount[key] ?? 0 + 1; @@ -38,71 +42,12 @@ class TestAssetBundle extends CachingAssetBundle { } } -class BundleWithoutAssetManifestBin extends CachingAssetBundle { - BundleWithoutAssetManifestBin(this._legacyAssetBundleMap); - - final Map> _legacyAssetBundleMap; - - Map loadCallCount = {}; - - @override - Future load(String key) async { - ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale); - - if (key == 'AssetManifest.bin') { - throw FlutterError('AssetManifest.bin was not found.'); - } - if (key == 'AssetManifest.json') { - return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(json.encode(_legacyAssetBundleMap))).buffer); - } - switch (key) { - case 'assets/image.png': - return testByteData(1.0); // see "...with a main asset and a 1.0x asset" - case 'assets/2.0x/image.png': - return testByteData(1.5); - } - - throw FlutterError('Unexpected key: $key'); - } - - @override - Future loadBuffer(String key) async { - final ByteData data = await load(key); - return ui.ImmutableBuffer.fromUint8List(data.buffer.asUint8List()); - } -} - void main() { - - // TODO(andrewkolos): Once google3 is migrated away from using AssetManifest.json, - // remove all references to it. See https://github.com/flutter/flutter/issues/114913. - test('AssetBundle falls back to using AssetManifest.json if AssetManifest.bin cannot be found.', () async { - const String assetPath = 'assets/image.png'; - final Map> assetBundleMap = >{}; - assetBundleMap[assetPath] = []; - final AssetImage assetImage = AssetImage(assetPath, bundle: BundleWithoutAssetManifestBin(assetBundleMap)); - final AssetBundleImageKey key = await assetImage.obtainKey(ImageConfiguration.empty); - expect(key.name, assetPath); - expect(key.scale, 1.0); - }); - - test('When using AssetManifest.json, on a high DPR device, a high dpr variant is selected.', () async { - const String assetPath = 'assets/image.png'; - const String asset2xPath = 'assets/2.0x/image.png'; - final Map> assetBundleMap = >{}; - assetBundleMap[assetPath] = [asset2xPath]; - final AssetImage assetImage = AssetImage(assetPath, bundle: BundleWithoutAssetManifestBin(assetBundleMap)); - final AssetBundleImageKey key = await assetImage.obtainKey(const ImageConfiguration(devicePixelRatio: 2.0)); - expect(key.name, asset2xPath); - expect(key.scale, 2.0); - }); - group('1.0 scale device tests', () { void buildAndTestWithOneAsset(String mainAssetPath) { - final Map>> assetBundleMap = - >>{}; + final Map> assetBundleMap = >{}; - assetBundleMap[mainAssetPath] = >[]; + assetBundleMap[mainAssetPath] = []; final AssetImage assetImage = AssetImage( mainAssetPath, @@ -148,13 +93,10 @@ void main() { const String mainAssetPath = 'assets/normalFolder/normalFile.png'; const String variantPath = 'assets/normalFolder/3.0x/normalFile.png'; - final Map>> assetBundleMap = - >>{}; + final Map> assetBundleMap = + >{}; - final Map mainAssetVariantManifestEntry = {}; - mainAssetVariantManifestEntry['asset'] = variantPath; - mainAssetVariantManifestEntry['dpr'] = 3.0; - assetBundleMap[mainAssetPath] = >[mainAssetVariantManifestEntry]; + assetBundleMap[mainAssetPath] = [mainAssetPath, variantPath]; final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap); @@ -181,10 +123,10 @@ void main() { test('When high-res device and high-res asset not present in bundle then return main variant', () { const String mainAssetPath = 'assets/normalFolder/normalFile.png'; - final Map>> assetBundleMap = - >>{}; + final Map> assetBundleMap = + >{}; - assetBundleMap[mainAssetPath] = >[]; + assetBundleMap[mainAssetPath] = [mainAssetPath]; final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap); @@ -214,18 +156,16 @@ void main() { const String mainAssetPath = 'assets/normalFolder/normalFile.png'; const String variantPath = 'assets/normalFolder/3.0x/normalFile.png'; + void buildBundleAndTestVariantLogic( double deviceRatio, double chosenAssetRatio, String expectedAssetPath, ) { - final Map>> assetBundleMap = - >>{}; + final Map> assetBundleMap = + >{}; - final Map mainAssetVariantManifestEntry = {}; - mainAssetVariantManifestEntry['asset'] = variantPath; - mainAssetVariantManifestEntry['dpr'] = 3.0; - assetBundleMap[mainAssetPath] = >[mainAssetVariantManifestEntry]; + assetBundleMap[mainAssetPath] = [mainAssetPath, variantPath]; final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap); diff --git a/packages/flutter/test/services/asset_bundle_test.dart b/packages/flutter/test/services/asset_bundle_test.dart index 4beac89d52b..8a97df3daa2 100644 --- a/packages/flutter/test/services/asset_bundle_test.dart +++ b/packages/flutter/test/services/asset_bundle_test.dart @@ -9,14 +9,14 @@ import 'package:flutter/painting.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -class _TestAssetBundle extends CachingAssetBundle { +class TestAssetBundle extends CachingAssetBundle { Map loadCallCount = {}; @override Future load(String key) async { loadCallCount[key] = loadCallCount[key] ?? 0 + 1; - if (key == 'AssetManifest.bin') { - return const StandardMessageCodec().encodeMessage(json.decode('{"one":[]}'))!; + if (key == 'AssetManifest.json') { + return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert('{"one": ["one"]}')).buffer); } if (key == 'one') { @@ -30,7 +30,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('Caching asset bundle test', () async { - final _TestAssetBundle bundle = _TestAssetBundle(); + final TestAssetBundle bundle = TestAssetBundle(); final ByteData assetData = await bundle.load('one'); expect(assetData.getInt8(0), equals(49)); @@ -53,7 +53,7 @@ void main() { test('AssetImage.obtainKey succeeds with ImageConfiguration.empty', () async { // This is a regression test for https://github.com/flutter/flutter/issues/12392 - final AssetImage assetImage = AssetImage('one', bundle: _TestAssetBundle()); + final AssetImage assetImage = AssetImage('one', bundle: TestAssetBundle()); final AssetBundleImageKey key = await assetImage.obtainKey(ImageConfiguration.empty); expect(key.name, 'one'); expect(key.scale, 1.0); diff --git a/packages/flutter/test/widgets/image_resolution_test.dart b/packages/flutter/test/widgets/image_resolution_test.dart index da6ac354693..644f2c47903 100644 --- a/packages/flutter/test/widgets/image_resolution_test.dart +++ b/packages/flutter/test/widgets/image_resolution_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. @TestOn('!chrome') -import 'dart:convert'; import 'dart:ui' as ui show Image; import 'package:flutter/foundation.dart'; @@ -17,32 +16,27 @@ import '../image_data.dart'; ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale); double scaleOf(ByteData data) => data.getFloat64(0); -final Map testManifest = json.decode(''' +const String testManifest = ''' { "assets/image.png" : [ - {"asset": "assets/1.5x/image.png", "dpr": 1.5}, - {"asset": "assets/2.0x/image.png", "dpr": 2.0}, - {"asset": "assets/3.0x/image.png", "dpr": 3.0}, - {"asset": "assets/4.0x/image.png", "dpr": 4.0} + "assets/image.png", + "assets/1.5x/image.png", + "assets/2.0x/image.png", + "assets/3.0x/image.png", + "assets/4.0x/image.png" ] } -''') as Map; +'''; class TestAssetBundle extends CachingAssetBundle { + TestAssetBundle({ this.manifest = testManifest }); - TestAssetBundle({ required Map manifest }) { - this.manifest = const StandardMessageCodec().encodeMessage(manifest)!; - } - - late final ByteData manifest; + final String manifest; @override Future load(String key) { late ByteData data; switch (key) { - case 'AssetManifest.bin': - data = manifest; - break; case 'assets/image.png': data = testByteData(1.0); break; @@ -65,6 +59,14 @@ class TestAssetBundle extends CachingAssetBundle { return SynchronousFuture(data); } + @override + Future loadString(String key, { bool cache = true }) { + if (key == 'AssetManifest.json') { + return SynchronousFuture(manifest); + } + return SynchronousFuture(''); + } + @override String toString() => '${describeIdentity(this)}()'; } @@ -104,7 +106,7 @@ Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize devicePixelRatio: ratio, ), child: DefaultAssetBundle( - bundle: bundle ?? TestAssetBundle(manifest: testManifest), + bundle: bundle ?? TestAssetBundle(), child: Center( child: inferSize ? Image( @@ -257,21 +259,46 @@ void main() { expect(getRenderImage(tester, key).scale, 4.0); }); + testWidgets('Image for device pixel ratio 1.0, with no main asset', (WidgetTester tester) async { + const String manifest = ''' + { + "assets/image.png" : [ + "assets/1.5x/image.png", + "assets/2.0x/image.png", + "assets/3.0x/image.png", + "assets/4.0x/image.png" + ] + } + '''; + final AssetBundle bundle = TestAssetBundle(manifest: manifest); + + const double ratio = 1.0; + Key key = GlobalKey(); + await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle)); + expect(getRenderImage(tester, key).size, const Size(200.0, 200.0)); + expect(getRenderImage(tester, key).scale, 1.5); + key = GlobalKey(); + await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle)); + expect(getRenderImage(tester, key).size, const Size(48.0, 48.0)); + expect(getRenderImage(tester, key).scale, 1.5); + }); + testWidgets('Image for device pixel ratio 1.0, with a main asset and a 1.0x asset', (WidgetTester tester) async { // If both a main asset and a 1.0x asset are specified, then prefer // the 1.0x asset. - final Map manifest = json.decode(''' + const String manifest = ''' { "assets/image.png" : [ - {"asset": "assets/1.0x/image.png", "dpr":1.0}, - {"asset": "assets/1.5x/image.png", "dpr":1.5}, - {"asset": "assets/2.0x/image.png", "dpr":2.0}, - {"asset": "assets/3.0x/image.png", "dpr":3.0}, - {"asset": "assets/4.0x/image.png", "dpr":4.0} + "assets/image.png", + "assets/1.0x/image.png", + "assets/1.5x/image.png", + "assets/2.0x/image.png", + "assets/3.0x/image.png", + "assets/4.0x/image.png" ] } - ''') as Map; + '''; final AssetBundle bundle = TestAssetBundle(manifest: manifest); const double ratio = 1.0; @@ -310,13 +337,14 @@ void main() { // if higher resolution assets are not available we will pick the best // available. testWidgets('Low-resolution assets', (WidgetTester tester) async { - final AssetBundle bundle = TestAssetBundle(manifest: json.decode(''' + final AssetBundle bundle = TestAssetBundle(manifest: ''' { "assets/image.png" : [ - {"asset": "assets/1.5x/image.png", "dpr": 1.5} + "assets/image.png", + "assets/1.5x/image.png" ] } - ''') as Map); + '''); Future testRatio({required double ratio, required double expectedScale}) async { Key key = GlobalKey(); diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 0aed9af10e6..ea0104f8ef4 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -2,11 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:typed_data'; - import 'package:meta/meta.dart'; import 'package:package_config/package_config.dart'; -import 'package:standard_message_codec/standard_message_codec.dart'; import 'base/context.dart'; import 'base/deferred_component.dart'; @@ -139,9 +136,6 @@ class ManifestAssetBundle implements AssetBundle { _splitDeferredAssets = splitDeferredAssets, _licenseCollector = LicenseCollector(fileSystem: fileSystem); - // We assume the main asset is designed for a device pixel ratio of 1.0 - static const double _defaultResolution = 1.0; - final Logger _logger; final FileSystem _fileSystem; final LicenseCollector _licenseCollector; @@ -167,8 +161,7 @@ class ManifestAssetBundle implements AssetBundle { DateTime? _lastBuildTimestamp; - static const String _kAssetManifestBinFileName = 'AssetManifest.bin'; - static const String _kAssetManifestJsonFileName = 'AssetManifest.json'; + static const String _kAssetManifestJson = 'AssetManifest.json'; static const String _kNoticeFile = 'NOTICES'; // Comically, this can't be name with the more common .gz file extension // because when it's part of an AAR and brought into another APK via gradle, @@ -236,13 +229,8 @@ class ManifestAssetBundle implements AssetBundle { // device. _lastBuildTimestamp = DateTime.now(); if (flutterManifest.isEmpty) { - entries[_kAssetManifestJsonFileName] = DevFSStringContent('{}'); - entryKinds[_kAssetManifestJsonFileName] = AssetKind.regular; - final ByteData emptyAssetManifest = - const StandardMessageCodec().encodeMessage({})!; - entries[_kAssetManifestBinFileName] = - DevFSByteContent(emptyAssetManifest.buffer.asUint8List(0, emptyAssetManifest.lengthInBytes)); - entryKinds[_kAssetManifestBinFileName] = AssetKind.regular; + entries[_kAssetManifestJson] = DevFSStringContent('{}'); + entryKinds[_kAssetManifestJson] = AssetKind.regular; return 0; } @@ -439,11 +427,7 @@ class ManifestAssetBundle implements AssetBundle { _wildcardDirectories[uri] ??= _fileSystem.directory(uri); } - final Map> assetManifest = - _createAssetManifest(assetVariants, deferredComponentsAssetVariants); - final DevFSStringContent assetManifestJson = DevFSStringContent(json.encode(assetManifest)); - final DevFSByteContent assetManifestBinary = _createAssetManifestBinary(assetManifest); - + final DevFSStringContent assetManifest = _createAssetManifest(assetVariants, deferredComponentsAssetVariants); final DevFSStringContent fontManifest = DevFSStringContent(json.encode(fonts)); final LicenseResult licenseResult = _licenseCollector.obtainLicenses(packageConfig, additionalLicenseFiles); if (licenseResult.errorMessages.isNotEmpty) { @@ -467,8 +451,7 @@ class ManifestAssetBundle implements AssetBundle { _fileSystem.file('DOES_NOT_EXIST_RERUN_FOR_WILDCARD$suffix').absolute); } - _setIfChanged(_kAssetManifestJsonFileName, assetManifestJson, AssetKind.regular); - _setIfChanged(_kAssetManifestBinFileName, assetManifestBinary, AssetKind.regular); + _setIfChanged(_kAssetManifestJson, assetManifest, AssetKind.regular); _setIfChanged(kFontManifestJson, fontManifest, AssetKind.regular); _setLicenseIfChanged(licenseResult.combinedLicenses, targetPlatform); return 0; @@ -477,31 +460,17 @@ class ManifestAssetBundle implements AssetBundle { @override List additionalDependencies = []; - void _setIfChanged(String key, DevFSContent content, AssetKind assetKind) { - bool areEqual(List o1, List o2) { - if (o1.length != o2.length) { - return false; - } - - for (int index = 0; index < o1.length; index++) { - if (o1[index] != o2[index]) { - return false; - } - } - - return true; - } - - final DevFSContent? oldContent = entries[key]; - // In the case that the content is unchanged, we want to avoid an overwrite - // as the isModified property may be reset to true, - if (oldContent is DevFSByteContent && content is DevFSByteContent && - areEqual(oldContent.bytes, content.bytes)) { + void _setIfChanged(String key, DevFSStringContent content, AssetKind assetKind) { + if (!entries.containsKey(key)) { + entries[key] = content; + entryKinds[key] = assetKind; return; } - - entries[key] = content; - entryKinds[key] = assetKind; + final DevFSStringContent? oldContent = entries[key] as DevFSStringContent?; + if (oldContent?.string != content.string) { + entries[key] = content; + entryKinds[key] = assetKind; + } } void _setLicenseIfChanged( @@ -653,14 +622,14 @@ class ManifestAssetBundle implements AssetBundle { return deferredComponentsAssetVariants; } - Map> _createAssetManifest( + DevFSStringContent _createAssetManifest( Map<_Asset, List<_Asset>> assetVariants, Map>> deferredComponentsAssetVariants ) { - final Map> manifest = >{}; - final Map<_Asset, List> entries = <_Asset, List>{}; + final Map> jsonObject = >{}; + final Map<_Asset, List> jsonEntries = <_Asset, List>{}; assetVariants.forEach((_Asset main, List<_Asset> variants) { - entries[main] = [ + jsonEntries[main] = [ for (final _Asset variant in variants) variant.entryUri.path, ]; @@ -668,69 +637,26 @@ class ManifestAssetBundle implements AssetBundle { if (deferredComponentsAssetVariants != null) { for (final Map<_Asset, List<_Asset>> componentAssets in deferredComponentsAssetVariants.values) { componentAssets.forEach((_Asset main, List<_Asset> variants) { - entries[main] = [ + jsonEntries[main] = [ for (final _Asset variant in variants) variant.entryUri.path, ]; }); } } - final List<_Asset> sortedKeys = entries.keys.toList() + final List<_Asset> sortedKeys = jsonEntries.keys.toList() ..sort((_Asset left, _Asset right) => left.entryUri.path.compareTo(right.entryUri.path)); for (final _Asset main in sortedKeys) { final String decodedEntryPath = Uri.decodeFull(main.entryUri.path); - final List rawEntryVariantsPaths = entries[main]!; + final List rawEntryVariantsPaths = jsonEntries[main]!; final List decodedEntryVariantPaths = rawEntryVariantsPaths .map((String value) => Uri.decodeFull(value)) .toList(); - manifest[decodedEntryPath] = decodedEntryVariantPaths; + jsonObject[decodedEntryPath] = decodedEntryVariantPaths; } - return manifest; + return DevFSStringContent(json.encode(jsonObject)); } - DevFSByteContent _createAssetManifestBinary( - Map> assetManifest - ) { - double parseScale(String key) { - final Uri assetUri = Uri.parse(key); - String directoryPath = ''; - if (assetUri.pathSegments.length > 1) { - directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2]; - } - - final Match? match = _extractRatioRegExp.firstMatch(directoryPath); - if (match != null && match.groupCount > 0) { - return double.parse(match.group(1)!); - } - return _defaultResolution; - } - - final Map result = {}; - - for (final MapEntry manifestEntry in assetManifest.entries) { - final List resultVariants = []; - final List entries = (manifestEntry.value as List).cast(); - for (final String variant in entries) { - if (variant == manifestEntry.key) { - // With the newer binary format, don't include the main asset in it's - // list of variants. This reduces parsing time at runtime. - continue; - } - final Map resultVariant = {}; - final double variantDevicePixelRatio = parseScale(variant); - resultVariant['asset'] = variant; - resultVariant['dpr'] = variantDevicePixelRatio; - resultVariants.add(resultVariant); - } - result[manifestEntry.key] = resultVariants; - } - - final ByteData message = const StandardMessageCodec().encodeMessage(result)!; - return DevFSByteContent(message.buffer.asUint8List(0, message.lengthInBytes)); - } - - static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$'); - /// Prefixes family names and asset paths of fonts included from packages with /// 'packages/' List _parsePackageFonts( diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index aa69dcc343a..91dafbe172c 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -57,8 +57,6 @@ dependencies: vm_service: 9.4.0 - standard_message_codec: 0.0.1+3 - _fe_analyzer_shared: 50.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" analyzer: 5.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -90,6 +88,7 @@ dependencies: watcher: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" dev_dependencies: + collection: 1.17.0 file_testing: 3.0.0 pubspec_parse: 1.2.1 @@ -98,10 +97,9 @@ dev_dependencies: json_annotation: 4.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_preamble: 2.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" test: 1.22.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - collection: 1.17.0 dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: 408d +# PUBSPEC CHECKSUM: 65eb diff --git a/packages/flutter_tools/test/general.shard/android/deferred_components_gen_snapshot_validator_test.dart b/packages/flutter_tools/test/general.shard/android/deferred_components_gen_snapshot_validator_test.dart index 9366653644a..6962b50a6aa 100644 --- a/packages/flutter_tools/test/general.shard/android/deferred_components_gen_snapshot_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/android/deferred_components_gen_snapshot_validator_test.dart @@ -220,7 +220,7 @@ loading-units-spelled-wrong: expect(logger.statusText, contains('Errors checking the following files:')); expect(logger.statusText, contains("Invalid loading units yaml file, 'loading-units' entry did not exist.")); - expect(logger.statusText, isNot(contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'))); + expect(logger.statusText.contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), false); }); testWithoutContext('loadingUnitCache validator detects malformed file: not a list', () async { @@ -382,7 +382,7 @@ loading-units: validator.displayResults(); validator.attemptToolExit(); - expect(logger.statusText, isNot(contains('Errors checking the following files:'))); + expect(logger.statusText.contains('Errors checking the following files:'), false); }); testWithoutContext('androidStringMapping modifies strings file', () async { @@ -448,10 +448,9 @@ loading-units: .childDirectory('main') .childFile('AndroidManifest.xml'); expect(manifestOutput.existsSync(), true); - final String manifestOutputString = manifestOutput.readAsStringSync(); - expect(manifestOutputString, contains('')); - expect(manifestOutputString, isNot(contains('android:value="invalidmapping"'))); - expect(manifestOutputString, contains("