Skip to content

Commit

Permalink
Reland "Add API for discovering assets" (#119277)
Browse files Browse the repository at this point in the history
* 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 <jonahwilliams@google.com>

* Update packages/flutter/lib/src/services/asset_manifest.dart

Co-authored-by: Jonah Williams <jonahwilliams@google.com>

* 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 <jonahwilliams@google.com>
  • Loading branch information
andrewkolos and jonahwilliams committed Feb 2, 2023
1 parent f9daa9a commit fd76ef0
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show PlatformAssetBundle;
import 'package:flutter/services.dart' show AssetManifest, PlatformAssetBundle, rootBundle;
import 'package:flutter/widgets.dart';

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

final ByteData assetManifestBytes = await bundle.load('money_asset_manifest.json');
watch.start();
for (int i = 0; i < _kNumIterations; i++) {
await AssetManifest.loadFromAssetBundle(bundle);
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
await AssetImage.manifestParser(json);
}
watch.stop();

Expand Down
2 changes: 1 addition & 1 deletion dev/devicelab/lib/versions/gallery.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
// found in the LICENSE file.

/// The pinned version of flutter gallery, used for devicelab tests.
const String galleryVersion = 'b6728704a6441ac37a21e433a1e43c990780d47b';
const String galleryVersion = 'afcf15fe40d8b9243bad30895d3ba1ad49014550';
1 change: 1 addition & 0 deletions packages/flutter/lib/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
library services;

export 'src/services/asset_bundle.dart';
export 'src/services/asset_manifest.dart';
export 'src/services/autofill.dart';
export 'src/services/binary_messenger.dart';
export 'src/services/binding.dart';
Expand Down
75 changes: 73 additions & 2 deletions packages/flutter/lib/src/services/asset_bundle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,22 @@ abstract class AssetBundle {
}

/// Retrieve a string from the asset bundle, parse it with the given function,
/// and return the function's result.
/// 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<T> loadStructuredData<T>(String key, Future<T> 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<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
final ByteData data = await load(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.
Expand Down Expand Up @@ -154,6 +164,16 @@ 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<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
return parser(await load(key));
}

// TODO(ianh): Once the underlying network logic learns about caching, we
// should implement evict().

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

@override
Future<String> loadString(String key, { bool cache = true }) {
Expand Down Expand Up @@ -221,16 +242,66 @@ 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<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) {
if (_structuredBinaryDataCache.containsKey(key)) {
return _structuredBinaryDataCache[key]! as Future<T>;
}

// 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<T>? completer; // For async flow.
SynchronousFuture<T>? result; // For sync flow.

load(key)
.then<T>(parser)
.then<void>((T value) {
result = SynchronousFuture<T>(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 error, StackTrace stack) {
completer!.completeError(error, 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<T>();
_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
Expand Down Expand Up @@ -272,7 +343,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 gaurded on
// above. If that code is changed, this needs to be guarded 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.
Expand Down
134 changes: 134 additions & 0 deletions packages/flutter/lib/src/services/asset_manifest.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';

import 'asset_bundle.dart';
import 'message_codecs.dart';

const String _kAssetManifestFilename = 'AssetManifest.bin';

/// Contains details about available assets and their variants.
/// See [Asset variants](https://docs.flutter.dev/development/ui/assets-and-images#asset-variants)
/// to learn about asset variants and how to declare them.
abstract class AssetManifest {
/// Loads asset manifest data from an [AssetBundle] object and creates an
/// [AssetManifest] object from that data.
static Future<AssetManifest> loadFromAssetBundle(AssetBundle bundle) {
return bundle.loadStructuredBinaryData(_kAssetManifestFilename, _AssetManifestBin.fromStandardMessageCodecMessage);
}

/// Lists the keys of all main assets. This does not include assets
/// that are variants of other assets.
///
/// The logical key maps to the path of an asset specified in the pubspec.yaml
/// file at build time.
///
/// See [Specifying assets](https://docs.flutter.dev/development/ui/assets-and-images#specifying-assets)
/// and [Loading assets](https://docs.flutter.dev/development/ui/assets-and-images#loading-assets) for more
/// information.
List<String> listAssets();

/// Retrieves metadata about an asset and its variants.
///
/// Note that this method considers a main asset to be a variant of itself and
/// includes it in the returned list.
///
/// Throws an [ArgumentError] if [key] cannot be found within the manifest. To
/// avoid this, use a key obtained from the [listAssets] method.
List<AssetMetadata> getAssetVariants(String key);
}

// Lazily parses the binary asset manifest into a data structure that's easier to work
// with.
//
// The binary asset manifest is a map of asset keys to a list of objects
// representing the asset's variants.
//
// 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<Object?, Object?> standardMessageData): _data = standardMessageData;

factory _AssetManifestBin.fromStandardMessageCodecMessage(ByteData message) {
final dynamic data = const StandardMessageCodec().decodeMessage(message);
return _AssetManifestBin(data as Map<Object?, Object?>);
}

final Map<Object?, Object?> _data;
final Map<String, List<AssetMetadata>> _typeCastedData = <String, List<AssetMetadata>>{};

@override
List<AssetMetadata> getAssetVariants(String key) {
// We lazily delay typecasting to prevent a performance hiccup when parsing
// large asset manifests. This is important to keep an app's first asset
// load fast.
if (!_typeCastedData.containsKey(key)) {
final Object? variantData = _data[key];
if (variantData == null) {
throw ArgumentError('Asset key $key was not found within the asset manifest.');
}
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
.cast<Map<Object?, Object?>>()
.map((Map<Object?, Object?> data) => AssetMetadata(
key: data['asset']! as String,
targetDevicePixelRatio: data['dpr']! as double,
main: false,
))
.toList();

_data.remove(key);
}

final AssetMetadata mainAsset = AssetMetadata(key: key,
targetDevicePixelRatio: null,
main: true
);

return <AssetMetadata>[mainAsset, ..._typeCastedData[key]!];
}

@override
List<String> listAssets() {
return <String>[..._data.keys.cast<String>(), ..._typeCastedData.keys];
}
}

/// Contains information about an asset.
@immutable
class AssetMetadata {
/// Creates an object containing information about an asset.
const AssetMetadata({
required this.key,
required this.targetDevicePixelRatio,
required this.main,
});

/// The device pixel ratio that this asset is most ideal for. This is determined
/// by the name of the parent folder of the asset file. For example, if the
/// parent folder is named "3.0x", the target device pixel ratio of that
/// asset will be interpreted as 3.
///
/// This will be null if the parent folder name is not a ratio value followed
/// by an "x".
///
/// See [Declaring resolution-aware image assets](https://docs.flutter.dev/development/ui/assets-and-images#resolution-aware)
/// for more information.
final double? targetDevicePixelRatio;

/// The asset's key, which is the path to the asset specified in the pubspec.yaml
/// file at build time.
final String key;

/// Whether or not this is a main asset. In other words, this is true if
/// this asset is not a variant of another asset.
///
/// See [Asset variants](https://docs.flutter.dev/development/ui/assets-and-images#asset-variants)
/// for more about asset variants.
final bool main;
}

0 comments on commit fd76ef0

Please sign in to comment.