Skip to content

Commit fd76ef0

Browse files
Reland "Add API for discovering assets" (#119277)
* add asset manifest bin loading and asset manifest api * use new api for image resolution * remove upfront smc data casting * fix typecasting issue * remove unused import * fix tests * lints * lints * fix import * revert image resolution changes * Update image_resolution_test.dart * Update decode_and_parse_asset_manifest.dart * make targetDevicePixelRatio optional * Update packages/flutter/lib/src/services/asset_manifest.dart Co-authored-by: Jonah Williams <[email protected]> * Update packages/flutter/lib/src/services/asset_manifest.dart Co-authored-by: Jonah Williams <[email protected]> * fix immutable not being imported * return List in AssetManifest methods, fix annotation import * simplify onError callback * make AssetManifest methods abstract instead of throwing UnimplementedError * simplify AssetVariant.key docstring * tweak _AssetManifestBin docstring * make AssetManifest and AssetVariant doc strings more specific * use List.of instead of List.from for type-safety * adjust import * change _AssetManifestBin comment from doc comment to normal comment * revert to callback function for onError in loadStructuredBinaryData * add more to the docstring of AssetManifest.listAssets and AssetVariant.key * add tests for CachingAssetBundle caching behavior * add simple test to ensure loadStructuredBinaryData correctly calls load * Update asset_manifest.dart * update docstring for AssetManifest.getAssetVariants * rename getAssetVariants, have it include main asset * rename isMainAsset field of AssetMetadata to main * (slightly) shorten name of describeAssetAndVariants * rename describeAssetVariants back to getAssetVariants * add tests for TestAssetBundle * nits * fix typo in docstring * remove no longer necessary non-null asserts * update gallery and google_fonts versions --------- Co-authored-by: Jonah Williams <[email protected]>
1 parent f9daa9a commit fd76ef0

File tree

7 files changed

+359
-15
lines changed

7 files changed

+359
-15
lines changed

dev/benchmarks/microbenchmarks/lib/foundation/decode_and_parse_asset_manifest.dart

+3-10
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'dart:convert';
6-
7-
import 'package:flutter/foundation.dart';
8-
import 'package:flutter/services.dart' show PlatformAssetBundle;
5+
import 'package:flutter/services.dart' show AssetManifest, PlatformAssetBundle, rootBundle;
96
import 'package:flutter/widgets.dart';
107

118
import '../common.dart';
@@ -18,16 +15,12 @@ void main() async {
1815
final BenchmarkResultPrinter printer = BenchmarkResultPrinter();
1916
WidgetsFlutterBinding.ensureInitialized();
2017
final Stopwatch watch = Stopwatch();
21-
final PlatformAssetBundle bundle = PlatformAssetBundle();
18+
final PlatformAssetBundle bundle = rootBundle as PlatformAssetBundle;
2219

23-
final ByteData assetManifestBytes = await bundle.load('money_asset_manifest.json');
2420
watch.start();
2521
for (int i = 0; i < _kNumIterations; i++) {
22+
await AssetManifest.loadFromAssetBundle(bundle);
2623
bundle.clear();
27-
final String json = utf8.decode(assetManifestBytes.buffer.asUint8List());
28-
// This is a test, so we don't need to worry about this rule.
29-
// ignore: invalid_use_of_visible_for_testing_member
30-
await AssetImage.manifestParser(json);
3124
}
3225
watch.stop();
3326

dev/devicelab/lib/versions/gallery.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
// found in the LICENSE file.
44

55
/// The pinned version of flutter gallery, used for devicelab tests.
6-
const String galleryVersion = 'b6728704a6441ac37a21e433a1e43c990780d47b';
6+
const String galleryVersion = 'afcf15fe40d8b9243bad30895d3ba1ad49014550';

packages/flutter/lib/services.dart

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
library services;
1212

1313
export 'src/services/asset_bundle.dart';
14+
export 'src/services/asset_manifest.dart';
1415
export 'src/services/autofill.dart';
1516
export 'src/services/binary_messenger.dart';
1617
export 'src/services/binding.dart';

packages/flutter/lib/src/services/asset_bundle.dart

+73-2
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,22 @@ abstract class AssetBundle {
9696
}
9797

9898
/// Retrieve a string from the asset bundle, parse it with the given function,
99-
/// and return the function's result.
99+
/// and return that function's result.
100100
///
101101
/// Implementations may cache the result, so a particular key should only be
102102
/// used with one parser for the lifetime of the asset bundle.
103103
Future<T> loadStructuredData<T>(String key, Future<T> Function(String value) parser);
104104

105+
/// Retrieve [ByteData] from the asset bundle, parse it with the given function,
106+
/// and return that function's result.
107+
///
108+
/// Implementations may cache the result, so a particular key should only be
109+
/// used with one parser for the lifetime of the asset bundle.
110+
Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
111+
final ByteData data = await load(key);
112+
return parser(data);
113+
}
114+
105115
/// If this is a caching asset bundle, and the given key describes a cached
106116
/// asset, then evict the asset from the cache so that the next time it is
107117
/// loaded, the cache will be reread from the asset bundle.
@@ -154,6 +164,16 @@ class NetworkAssetBundle extends AssetBundle {
154164
return parser(await loadString(key));
155165
}
156166

167+
/// Retrieve [ByteData] from the asset bundle, parse it with the given function,
168+
/// and return the function's result.
169+
///
170+
/// The result is not cached. The parser is run each time the resource is
171+
/// fetched.
172+
@override
173+
Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
174+
return parser(await load(key));
175+
}
176+
157177
// TODO(ianh): Once the underlying network logic learns about caching, we
158178
// should implement evict().
159179

@@ -173,6 +193,7 @@ abstract class CachingAssetBundle extends AssetBundle {
173193
// TODO(ianh): Replace this with an intelligent cache, see https://github.com/flutter/flutter/issues/3568
174194
final Map<String, Future<String>> _stringCache = <String, Future<String>>{};
175195
final Map<String, Future<dynamic>> _structuredDataCache = <String, Future<dynamic>>{};
196+
final Map<String, Future<dynamic>> _structuredBinaryDataCache = <String, Future<dynamic>>{};
176197

177198
@override
178199
Future<String> loadString(String key, { bool cache = true }) {
@@ -221,16 +242,66 @@ abstract class CachingAssetBundle extends AssetBundle {
221242
return completer.future;
222243
}
223244

245+
/// Retrieve bytedata from the asset bundle, parse it with the given function,
246+
/// and return the function's result.
247+
///
248+
/// The result of parsing the bytedata is cached (the bytedata itself is not).
249+
/// For any given `key`, the `parser` is only run the first time.
250+
///
251+
/// Once the value has been parsed, the future returned by this function for
252+
/// subsequent calls will be a [SynchronousFuture], which resolves its
253+
/// callback synchronously.
254+
@override
255+
Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) {
256+
if (_structuredBinaryDataCache.containsKey(key)) {
257+
return _structuredBinaryDataCache[key]! as Future<T>;
258+
}
259+
260+
// load can return a SynchronousFuture in certain cases, like in the
261+
// flutter_test framework. So, we need to support both async and sync flows.
262+
Completer<T>? completer; // For async flow.
263+
SynchronousFuture<T>? result; // For sync flow.
264+
265+
load(key)
266+
.then<T>(parser)
267+
.then<void>((T value) {
268+
result = SynchronousFuture<T>(value);
269+
if (completer != null) {
270+
// The load and parse operation ran asynchronously. We already returned
271+
// from the loadStructuredBinaryData function and therefore the caller
272+
// was given the future of the completer.
273+
completer.complete(value);
274+
}
275+
}, onError: (Object error, StackTrace stack) {
276+
completer!.completeError(error, stack);
277+
});
278+
279+
if (result != null) {
280+
// The above code ran synchronously. We can synchronously return the result.
281+
_structuredBinaryDataCache[key] = result!;
282+
return result!;
283+
}
284+
285+
// Since the above code is being run asynchronously and thus hasn't run its
286+
// `then` handler yet, we'll return a completer that will be completed
287+
// when the handler does run.
288+
completer = Completer<T>();
289+
_structuredBinaryDataCache[key] = completer.future;
290+
return completer.future;
291+
}
292+
224293
@override
225294
void evict(String key) {
226295
_stringCache.remove(key);
227296
_structuredDataCache.remove(key);
297+
_structuredBinaryDataCache.remove(key);
228298
}
229299

230300
@override
231301
void clear() {
232302
_stringCache.clear();
233303
_structuredDataCache.clear();
304+
_structuredBinaryDataCache.clear();
234305
}
235306

236307
@override
@@ -272,7 +343,7 @@ class PlatformAssetBundle extends CachingAssetBundle {
272343
bool debugUsePlatformChannel = false;
273344
assert(() {
274345
// dart:io is safe to use here since we early return for web
275-
// above. If that code is changed, this needs to be gaurded on
346+
// above. If that code is changed, this needs to be guarded on
276347
// web presence. Override how assets are loaded in tests so that
277348
// the old loader behavior that allows tests to load assets from
278349
// the current package using the package prefix.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/foundation.dart';
6+
7+
import 'asset_bundle.dart';
8+
import 'message_codecs.dart';
9+
10+
const String _kAssetManifestFilename = 'AssetManifest.bin';
11+
12+
/// Contains details about available assets and their variants.
13+
/// See [Asset variants](https://docs.flutter.dev/development/ui/assets-and-images#asset-variants)
14+
/// to learn about asset variants and how to declare them.
15+
abstract class AssetManifest {
16+
/// Loads asset manifest data from an [AssetBundle] object and creates an
17+
/// [AssetManifest] object from that data.
18+
static Future<AssetManifest> loadFromAssetBundle(AssetBundle bundle) {
19+
return bundle.loadStructuredBinaryData(_kAssetManifestFilename, _AssetManifestBin.fromStandardMessageCodecMessage);
20+
}
21+
22+
/// Lists the keys of all main assets. This does not include assets
23+
/// that are variants of other assets.
24+
///
25+
/// The logical key maps to the path of an asset specified in the pubspec.yaml
26+
/// file at build time.
27+
///
28+
/// See [Specifying assets](https://docs.flutter.dev/development/ui/assets-and-images#specifying-assets)
29+
/// and [Loading assets](https://docs.flutter.dev/development/ui/assets-and-images#loading-assets) for more
30+
/// information.
31+
List<String> listAssets();
32+
33+
/// Retrieves metadata about an asset and its variants.
34+
///
35+
/// Note that this method considers a main asset to be a variant of itself and
36+
/// includes it in the returned list.
37+
///
38+
/// Throws an [ArgumentError] if [key] cannot be found within the manifest. To
39+
/// avoid this, use a key obtained from the [listAssets] method.
40+
List<AssetMetadata> getAssetVariants(String key);
41+
}
42+
43+
// Lazily parses the binary asset manifest into a data structure that's easier to work
44+
// with.
45+
//
46+
// The binary asset manifest is a map of asset keys to a list of objects
47+
// representing the asset's variants.
48+
//
49+
// The entries with each variant object are:
50+
// - "asset": the location of this variant to load it from.
51+
// - "dpr": The device-pixel-ratio that the asset is best-suited for.
52+
//
53+
// New fields could be added to this object schema to support new asset variation
54+
// features, such as themes, locale/region support, reading directions, and so on.
55+
class _AssetManifestBin implements AssetManifest {
56+
_AssetManifestBin(Map<Object?, Object?> standardMessageData): _data = standardMessageData;
57+
58+
factory _AssetManifestBin.fromStandardMessageCodecMessage(ByteData message) {
59+
final dynamic data = const StandardMessageCodec().decodeMessage(message);
60+
return _AssetManifestBin(data as Map<Object?, Object?>);
61+
}
62+
63+
final Map<Object?, Object?> _data;
64+
final Map<String, List<AssetMetadata>> _typeCastedData = <String, List<AssetMetadata>>{};
65+
66+
@override
67+
List<AssetMetadata> getAssetVariants(String key) {
68+
// We lazily delay typecasting to prevent a performance hiccup when parsing
69+
// large asset manifests. This is important to keep an app's first asset
70+
// load fast.
71+
if (!_typeCastedData.containsKey(key)) {
72+
final Object? variantData = _data[key];
73+
if (variantData == null) {
74+
throw ArgumentError('Asset key $key was not found within the asset manifest.');
75+
}
76+
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
77+
.cast<Map<Object?, Object?>>()
78+
.map((Map<Object?, Object?> data) => AssetMetadata(
79+
key: data['asset']! as String,
80+
targetDevicePixelRatio: data['dpr']! as double,
81+
main: false,
82+
))
83+
.toList();
84+
85+
_data.remove(key);
86+
}
87+
88+
final AssetMetadata mainAsset = AssetMetadata(key: key,
89+
targetDevicePixelRatio: null,
90+
main: true
91+
);
92+
93+
return <AssetMetadata>[mainAsset, ..._typeCastedData[key]!];
94+
}
95+
96+
@override
97+
List<String> listAssets() {
98+
return <String>[..._data.keys.cast<String>(), ..._typeCastedData.keys];
99+
}
100+
}
101+
102+
/// Contains information about an asset.
103+
@immutable
104+
class AssetMetadata {
105+
/// Creates an object containing information about an asset.
106+
const AssetMetadata({
107+
required this.key,
108+
required this.targetDevicePixelRatio,
109+
required this.main,
110+
});
111+
112+
/// The device pixel ratio that this asset is most ideal for. This is determined
113+
/// by the name of the parent folder of the asset file. For example, if the
114+
/// parent folder is named "3.0x", the target device pixel ratio of that
115+
/// asset will be interpreted as 3.
116+
///
117+
/// This will be null if the parent folder name is not a ratio value followed
118+
/// by an "x".
119+
///
120+
/// See [Declaring resolution-aware image assets](https://docs.flutter.dev/development/ui/assets-and-images#resolution-aware)
121+
/// for more information.
122+
final double? targetDevicePixelRatio;
123+
124+
/// The asset's key, which is the path to the asset specified in the pubspec.yaml
125+
/// file at build time.
126+
final String key;
127+
128+
/// Whether or not this is a main asset. In other words, this is true if
129+
/// this asset is not a variant of another asset.
130+
///
131+
/// See [Asset variants](https://docs.flutter.dev/development/ui/assets-and-images#asset-variants)
132+
/// for more about asset variants.
133+
final bool main;
134+
}

0 commit comments

Comments
 (0)