Reland "Speed up first asset load by using the binary-formatted asset manifest for image resolution" (#122505)
Reland "Speed up first asset load by using the binary-formatted asset manifest for image resolution"
This commit is contained in:
parent
e08d250eeb
commit
313b01658a
@ -4,15 +4,12 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'image_provider.dart';
|
||||
|
||||
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
|
||||
/// laptops, desktop screens up to QHD, low-end tablets such as Kindle Fire).
|
||||
@ -284,18 +281,18 @@ class AssetImage extends AssetBundleImageProvider {
|
||||
Completer<AssetBundleImageKey>? completer;
|
||||
Future<AssetBundleImageKey>? result;
|
||||
|
||||
chosenBundle.loadStructuredData<Map<String, List<String>>?>(_kAssetManifestFileName, manifestParser).then<void>(
|
||||
(Map<String, List<String>>? manifest) {
|
||||
final String chosenName = _chooseVariant(
|
||||
AssetManifest.loadFromAssetBundle(chosenBundle)
|
||||
.then((AssetManifest manifest) {
|
||||
final Iterable<AssetMetadata>? candidateVariants = manifest.getAssetVariants(keyName);
|
||||
final AssetMetadata chosenVariant = _chooseVariant(
|
||||
keyName,
|
||||
configuration,
|
||||
manifest == null ? null : manifest[keyName],
|
||||
)!;
|
||||
final double chosenScale = _parseScale(chosenName);
|
||||
candidateVariants,
|
||||
);
|
||||
final AssetBundleImageKey key = AssetBundleImageKey(
|
||||
bundle: chosenBundle,
|
||||
name: chosenName,
|
||||
scale: chosenScale,
|
||||
name: chosenVariant.key,
|
||||
scale: chosenVariant.targetDevicePixelRatio ?? _naturalResolution,
|
||||
);
|
||||
if (completer != null) {
|
||||
// We already returned from this function, which means we are in the
|
||||
@ -309,14 +306,15 @@ class AssetImage extends AssetBundleImageProvider {
|
||||
// ourselves.
|
||||
result = SynchronousFuture<AssetBundleImageKey>(key);
|
||||
}
|
||||
},
|
||||
).catchError((Object error, StackTrace stack) {
|
||||
})
|
||||
.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);
|
||||
});
|
||||
|
||||
if (result != null) {
|
||||
// The code above ran synchronously, and came up with an answer.
|
||||
// Return the SynchronousFuture that we created above.
|
||||
@ -328,35 +326,24 @@ class AssetImage extends AssetBundleImageProvider {
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Parses the asset manifest string into a strongly-typed map.
|
||||
@visibleForTesting
|
||||
static Future<Map<String, List<String>>?> manifestParser(String? jsonData) {
|
||||
if (jsonData == null) {
|
||||
return SynchronousFuture<Map<String, List<String>>?>(null);
|
||||
}
|
||||
// TODO(ianh): JSON decoding really shouldn't be on the main thread.
|
||||
final Map<String, dynamic> parsedJson = json.decode(jsonData) as Map<String, dynamic>;
|
||||
final Iterable<String> keys = parsedJson.keys;
|
||||
final Map<String, List<String>> parsedManifest = <String, List<String>> {
|
||||
for (final String key in keys) key: List<String>.from(parsedJson[key] as List<dynamic>),
|
||||
};
|
||||
// TODO(ianh): convert that data structure to the right types.
|
||||
return SynchronousFuture<Map<String, List<String>>?>(parsedManifest);
|
||||
AssetMetadata _chooseVariant(String mainAssetKey, ImageConfiguration config, Iterable<AssetMetadata>? candidateVariants) {
|
||||
if (candidateVariants == null) {
|
||||
return AssetMetadata(key: mainAssetKey, targetDevicePixelRatio: null, main: true);
|
||||
}
|
||||
|
||||
String? _chooseVariant(String main, ImageConfiguration config, List<String>? candidates) {
|
||||
if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) {
|
||||
return main;
|
||||
if (config.devicePixelRatio == null) {
|
||||
return candidateVariants.firstWhere((AssetMetadata variant) => variant.main);
|
||||
}
|
||||
// TODO(ianh): Consider moving this parsing logic into _manifestParser.
|
||||
final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>();
|
||||
for (final String candidate in candidates) {
|
||||
mapping[_parseScale(candidate)] = candidate;
|
||||
|
||||
final SplayTreeMap<double, AssetMetadata> candidatesByDevicePixelRatio =
|
||||
SplayTreeMap<double, AssetMetadata>();
|
||||
for (final AssetMetadata candidate in candidateVariants) {
|
||||
candidatesByDevicePixelRatio[candidate.targetDevicePixelRatio ?? _naturalResolution] = candidate;
|
||||
}
|
||||
// TODO(ianh): implement support for config.locale, config.textDirection,
|
||||
// config.size, config.platform (then document this over in the Image.asset
|
||||
// docs)
|
||||
return _findBestVariant(mapping, config.devicePixelRatio!);
|
||||
return _findBestVariant(candidatesByDevicePixelRatio, config.devicePixelRatio!);
|
||||
}
|
||||
|
||||
// Returns the "best" asset variant amongst the available `candidates`.
|
||||
@ -371,17 +358,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`.
|
||||
String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) {
|
||||
if (candidates.containsKey(value)) {
|
||||
return candidates[value]!;
|
||||
AssetMetadata _findBestVariant(SplayTreeMap<double, AssetMetadata> candidatesByDpr, double value) {
|
||||
if (candidatesByDpr.containsKey(value)) {
|
||||
return candidatesByDpr[value]!;
|
||||
}
|
||||
final double? lower = candidates.lastKeyBefore(value);
|
||||
final double? upper = candidates.firstKeyAfter(value);
|
||||
final double? lower = candidatesByDpr.lastKeyBefore(value);
|
||||
final double? upper = candidatesByDpr.firstKeyAfter(value);
|
||||
if (lower == null) {
|
||||
return candidates[upper];
|
||||
return candidatesByDpr[upper]!;
|
||||
}
|
||||
if (upper == null) {
|
||||
return candidates[lower];
|
||||
return candidatesByDpr[lower]!;
|
||||
}
|
||||
|
||||
// On screens with low device-pixel ratios the artifacts from upscaling
|
||||
@ -389,32 +376,12 @@ 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 candidates[upper];
|
||||
return candidatesByDpr[upper]!;
|
||||
} else {
|
||||
return candidates[lower];
|
||||
return candidatesByDpr[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) {
|
||||
|
@ -266,6 +266,7 @@ abstract class CachingAssetBundle extends AssetBundle {
|
||||
.then<T>(parser)
|
||||
.then<void>((T value) {
|
||||
result = SynchronousFuture<T>(value);
|
||||
_structuredBinaryDataCache[key] = result!;
|
||||
if (completer != null) {
|
||||
// The load and parse operation ran asynchronously. We already returned
|
||||
// from the loadStructuredBinaryData function and therefore the caller
|
||||
@ -278,7 +279,6 @@ abstract class CachingAssetBundle extends AssetBundle {
|
||||
|
||||
if (result != null) {
|
||||
// The above code ran synchronously. We can synchronously return the result.
|
||||
_structuredBinaryDataCache[key] = result!;
|
||||
return result!;
|
||||
}
|
||||
|
||||
|
@ -30,14 +30,12 @@ abstract class AssetManifest {
|
||||
/// information.
|
||||
List<String> listAssets();
|
||||
|
||||
/// Retrieves metadata about an asset and its variants.
|
||||
/// Retrieves metadata about an asset and its variants. Returns null if the
|
||||
/// key was not found in the asset manifest.
|
||||
///
|
||||
/// 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);
|
||||
List<AssetMetadata>? getAssetVariants(String key);
|
||||
}
|
||||
|
||||
// Lazily parses the binary asset manifest into a data structure that's easier to work
|
||||
@ -64,14 +62,14 @@ class _AssetManifestBin implements AssetManifest {
|
||||
final Map<String, List<AssetMetadata>> _typeCastedData = <String, List<AssetMetadata>>{};
|
||||
|
||||
@override
|
||||
List<AssetMetadata> getAssetVariants(String key) {
|
||||
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.');
|
||||
return null;
|
||||
}
|
||||
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
|
||||
.cast<Map<Object?, Object?>>()
|
||||
|
@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -13,18 +12,14 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
class TestAssetBundle extends CachingAssetBundle {
|
||||
TestAssetBundle(this._assetBundleMap);
|
||||
|
||||
final Map<String, List<String>> _assetBundleMap;
|
||||
final Map<String, List<Map<Object?, Object?>>> _assetBundleMap;
|
||||
|
||||
Map<String, int> loadCallCount = <String, int>{};
|
||||
|
||||
String get _assetBundleContents {
|
||||
return json.encode(_assetBundleMap);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ByteData> load(String key) async {
|
||||
if (key == 'AssetManifest.json') {
|
||||
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(_assetBundleContents)).buffer);
|
||||
if (key == 'AssetManifest.bin') {
|
||||
return const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
|
||||
}
|
||||
|
||||
loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
|
||||
@ -45,9 +40,10 @@ class TestAssetBundle extends CachingAssetBundle {
|
||||
void main() {
|
||||
group('1.0 scale device tests', () {
|
||||
void buildAndTestWithOneAsset(String mainAssetPath) {
|
||||
final Map<String, List<String>> assetBundleMap = <String, List<String>>{};
|
||||
final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
|
||||
<String, List<Map<Object?, Object?>>>{};
|
||||
|
||||
assetBundleMap[mainAssetPath] = <String>[];
|
||||
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[];
|
||||
|
||||
final AssetImage assetImage = AssetImage(
|
||||
mainAssetPath,
|
||||
@ -93,11 +89,13 @@ void main() {
|
||||
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
|
||||
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
|
||||
|
||||
final Map<String, List<String>> assetBundleMap =
|
||||
<String, List<String>>{};
|
||||
|
||||
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
|
||||
final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
|
||||
<String, List<Map<Object?, Object?>>>{};
|
||||
|
||||
final Map<Object?, Object?> mainAssetVariantManifestEntry = <Object?, Object?>{};
|
||||
mainAssetVariantManifestEntry['asset'] = variantPath;
|
||||
mainAssetVariantManifestEntry['dpr'] = 3.0;
|
||||
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[mainAssetVariantManifestEntry];
|
||||
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
|
||||
|
||||
final AssetImage assetImage = AssetImage(
|
||||
@ -123,10 +121,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<String, List<String>> assetBundleMap =
|
||||
<String, List<String>>{};
|
||||
final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
|
||||
<String, List<Map<Object?, Object?>>>{};
|
||||
|
||||
assetBundleMap[mainAssetPath] = <String>[mainAssetPath];
|
||||
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[];
|
||||
|
||||
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
|
||||
|
||||
@ -162,10 +160,13 @@ void main() {
|
||||
double chosenAssetRatio,
|
||||
String expectedAssetPath,
|
||||
) {
|
||||
final Map<String, List<String>> assetBundleMap =
|
||||
<String, List<String>>{};
|
||||
final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
|
||||
<String, List<Map<Object?, Object?>>>{};
|
||||
|
||||
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
|
||||
final Map<Object?, Object?> mainAssetVariantManifestEntry = <Object?, Object?>{};
|
||||
mainAssetVariantManifestEntry['asset'] = variantPath;
|
||||
mainAssetVariantManifestEntry['dpr'] = 3.0;
|
||||
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[mainAssetVariantManifestEntry];
|
||||
|
||||
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
|
||||
|
||||
|
@ -35,7 +35,6 @@ class TestAssetBundle extends CachingAssetBundle {
|
||||
|
||||
throw FlutterError('key not found');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void main() {
|
||||
@ -63,6 +62,81 @@ void main() {
|
||||
expect(loadException, isFlutterError);
|
||||
});
|
||||
|
||||
group('CachingAssetBundle caching behavior', () {
|
||||
test('caches results for loadString, loadStructuredData, and loadBinaryStructuredData', () async {
|
||||
final TestAssetBundle bundle = TestAssetBundle();
|
||||
|
||||
final String firstLoadStringResult = await bundle.loadString('counter');
|
||||
final String secondLoadStringResult = await bundle.loadString('counter');
|
||||
expect(firstLoadStringResult, '1');
|
||||
expect(secondLoadStringResult, '1');
|
||||
|
||||
final String firstLoadStructuredDataResult = await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('one'));
|
||||
final String secondLoadStructuredDataResult = await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('two'));
|
||||
expect(firstLoadStructuredDataResult, 'one');
|
||||
expect(secondLoadStructuredDataResult, 'one');
|
||||
|
||||
final String firstLoadStructuredBinaryDataResult = await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('one'));
|
||||
final String secondLoadStructuredBinaryDataResult = await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('two'));
|
||||
expect(firstLoadStructuredBinaryDataResult, 'one');
|
||||
expect(secondLoadStructuredBinaryDataResult, 'one');
|
||||
});
|
||||
|
||||
test("clear clears all cached values'", () async {
|
||||
final TestAssetBundle bundle = TestAssetBundle();
|
||||
|
||||
await bundle.loadString('counter');
|
||||
bundle.clear();
|
||||
final String secondLoadStringResult = await bundle.loadString('counter');
|
||||
expect(secondLoadStringResult, '2');
|
||||
|
||||
await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('one'));
|
||||
bundle.clear();
|
||||
final String secondLoadStructuredDataResult = await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('two'));
|
||||
expect(secondLoadStructuredDataResult, 'two');
|
||||
|
||||
await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('one'));
|
||||
bundle.clear();
|
||||
final String secondLoadStructuredBinaryDataResult = await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('two'));
|
||||
expect(secondLoadStructuredBinaryDataResult, 'two');
|
||||
});
|
||||
|
||||
test('evict evicts a particular key from the cache', () async {
|
||||
final TestAssetBundle bundle = TestAssetBundle();
|
||||
|
||||
await bundle.loadString('counter');
|
||||
bundle.evict('counter');
|
||||
final String secondLoadStringResult = await bundle.loadString('counter');
|
||||
expect(secondLoadStringResult, '2');
|
||||
|
||||
await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('one'));
|
||||
bundle.evict('AssetManifest.json');
|
||||
final String secondLoadStructuredDataResult = await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('two'));
|
||||
expect(secondLoadStructuredDataResult, 'two');
|
||||
|
||||
await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('one'));
|
||||
bundle.evict('AssetManifest.bin');
|
||||
final String secondLoadStructuredBinaryDataResult = await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('two'));
|
||||
expect(secondLoadStructuredBinaryDataResult, 'two');
|
||||
});
|
||||
|
||||
test('for a given key, subsequent loadStructuredData calls are synchronous after the first call resolves', () async {
|
||||
final TestAssetBundle bundle = TestAssetBundle();
|
||||
await bundle.loadStructuredData('one', (String data) => SynchronousFuture<int>(1));
|
||||
final Future<int> data = bundle.loadStructuredData('one', (String data) => SynchronousFuture<int>(2));
|
||||
expect(data, isA<SynchronousFuture<int>>());
|
||||
expect(await data, 1);
|
||||
});
|
||||
|
||||
test('for a given key, subsequent loadStructuredBinaryData calls are synchronous after the first call resolves', () async {
|
||||
final TestAssetBundle bundle = TestAssetBundle();
|
||||
await bundle.loadStructuredBinaryData('one', (ByteData data) => 1);
|
||||
final Future<int> data = bundle.loadStructuredBinaryData('one', (ByteData data) => 2);
|
||||
expect(data, isA<SynchronousFuture<int>>());
|
||||
expect(await data, 1);
|
||||
});
|
||||
});
|
||||
|
||||
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());
|
||||
@ -114,63 +188,6 @@ void main() {
|
||||
);
|
||||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56314
|
||||
|
||||
test('CachingAssetBundle caches results for loadString, loadStructuredData, and loadBinaryStructuredData', () async {
|
||||
final TestAssetBundle bundle = TestAssetBundle();
|
||||
|
||||
final String firstLoadStringResult = await bundle.loadString('counter');
|
||||
final String secondLoadStringResult = await bundle.loadString('counter');
|
||||
expect(firstLoadStringResult, '1');
|
||||
expect(secondLoadStringResult, '1');
|
||||
|
||||
final String firstLoadStructuredDataResult = await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('one'));
|
||||
final String secondLoadStructuredDataResult = await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('two'));
|
||||
expect(firstLoadStructuredDataResult, 'one');
|
||||
expect(secondLoadStructuredDataResult, 'one');
|
||||
|
||||
final String firstLoadStructuredBinaryDataResult = await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('one'));
|
||||
final String secondLoadStructuredBinaryDataResult = await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('two'));
|
||||
expect(firstLoadStructuredBinaryDataResult, 'one');
|
||||
expect(secondLoadStructuredBinaryDataResult, 'one');
|
||||
});
|
||||
|
||||
test("CachingAssetBundle.clear clears all cached values'", () async {
|
||||
final TestAssetBundle bundle = TestAssetBundle();
|
||||
|
||||
await bundle.loadString('counter');
|
||||
bundle.clear();
|
||||
final String secondLoadStringResult = await bundle.loadString('counter');
|
||||
expect(secondLoadStringResult, '2');
|
||||
|
||||
await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('one'));
|
||||
bundle.clear();
|
||||
final String secondLoadStructuredDataResult = await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('two'));
|
||||
expect(secondLoadStructuredDataResult, 'two');
|
||||
|
||||
await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('one'));
|
||||
bundle.clear();
|
||||
final String secondLoadStructuredBinaryDataResult = await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('two'));
|
||||
expect(secondLoadStructuredBinaryDataResult, 'two');
|
||||
});
|
||||
|
||||
test('CachingAssetBundle.evict evicts a particular key from the cache', () async {
|
||||
final TestAssetBundle bundle = TestAssetBundle();
|
||||
|
||||
await bundle.loadString('counter');
|
||||
bundle.evict('counter');
|
||||
final String secondLoadStringResult = await bundle.loadString('counter');
|
||||
expect(secondLoadStringResult, '2');
|
||||
|
||||
await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('one'));
|
||||
bundle.evict('AssetManifest.json');
|
||||
final String secondLoadStructuredDataResult = await bundle.loadStructuredData('AssetManifest.json', (String value) => Future<String>.value('two'));
|
||||
expect(secondLoadStructuredDataResult, 'two');
|
||||
|
||||
await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('one'));
|
||||
bundle.evict('AssetManifest.bin');
|
||||
final String secondLoadStructuredBinaryDataResult = await bundle.loadStructuredBinaryData('AssetManifest.bin', (ByteData value) => Future<String>.value('two'));
|
||||
expect(secondLoadStructuredBinaryDataResult, 'two');
|
||||
});
|
||||
|
||||
test('loadStructuredBinaryData correctly loads ByteData', () async {
|
||||
final TestAssetBundle bundle = TestAssetBundle();
|
||||
final Map<Object?, Object?> assetManifest =
|
||||
|
@ -41,7 +41,7 @@ void main() {
|
||||
|
||||
expect(manifest.listAssets(), unorderedEquals(<String>['assets/foo.png', 'assets/bar.png']));
|
||||
|
||||
final List<AssetMetadata> fooVariants = manifest.getAssetVariants('assets/foo.png');
|
||||
final List<AssetMetadata> fooVariants = manifest.getAssetVariants('assets/foo.png')!;
|
||||
expect(fooVariants.length, 2);
|
||||
final AssetMetadata firstFooVariant = fooVariants[0];
|
||||
expect(firstFooVariant.key, 'assets/foo.png');
|
||||
@ -52,7 +52,7 @@ void main() {
|
||||
expect(secondFooVariant.targetDevicePixelRatio, 2.0);
|
||||
expect(secondFooVariant.main, false);
|
||||
|
||||
final List<AssetMetadata> barVariants = manifest.getAssetVariants('assets/bar.png');
|
||||
final List<AssetMetadata> barVariants = manifest.getAssetVariants('assets/bar.png')!;
|
||||
expect(barVariants.length, 1);
|
||||
final AssetMetadata firstBarVariant = barVariants[0];
|
||||
expect(firstBarVariant.key, 'assets/bar.png');
|
||||
@ -60,9 +60,8 @@ void main() {
|
||||
expect(firstBarVariant.main, true);
|
||||
});
|
||||
|
||||
test('getAssetVariants throws if given a key not contained in the asset manifest', () async {
|
||||
test('getAssetVariants returns null if the key not contained in the asset manifest', () async {
|
||||
final AssetManifest manifest = await AssetManifest.loadFromAssetBundle(TestAssetBundle());
|
||||
|
||||
expect(() => manifest.getAssetVariants('invalid asset key'), throwsArgumentError);
|
||||
expect(manifest.getAssetVariants('invalid asset key'), isNull);
|
||||
});
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
@TestOn('!chrome')
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui show Image;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -18,27 +19,31 @@ import '../image_data.dart';
|
||||
ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
|
||||
double scaleOf(ByteData data) => data.getFloat64(0);
|
||||
|
||||
const String testManifest = '''
|
||||
final Map<Object?, Object?> testManifest = json.decode('''
|
||||
{
|
||||
"assets/image.png" : [
|
||||
"assets/image.png",
|
||||
"assets/1.5x/image.png",
|
||||
"assets/2.0x/image.png",
|
||||
"assets/3.0x/image.png",
|
||||
"assets/4.0x/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}
|
||||
]
|
||||
}
|
||||
''';
|
||||
''') as Map<Object?, Object?>;
|
||||
|
||||
class TestAssetBundle extends CachingAssetBundle {
|
||||
TestAssetBundle({ this.manifest = testManifest });
|
||||
TestAssetBundle({ required Map<Object?, Object?> manifest }) {
|
||||
this.manifest = const StandardMessageCodec().encodeMessage(manifest)!;
|
||||
}
|
||||
|
||||
final String manifest;
|
||||
late final ByteData manifest;
|
||||
|
||||
@override
|
||||
Future<ByteData> load(String key) {
|
||||
late ByteData data;
|
||||
switch (key) {
|
||||
case 'AssetManifest.bin':
|
||||
data = manifest;
|
||||
break;
|
||||
case 'assets/image.png':
|
||||
data = testByteData(1.0);
|
||||
break;
|
||||
@ -61,14 +66,6 @@ class TestAssetBundle extends CachingAssetBundle {
|
||||
return SynchronousFuture<ByteData>(data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> loadString(String key, { bool cache = true }) {
|
||||
if (key == 'AssetManifest.json') {
|
||||
return SynchronousFuture<String>(manifest);
|
||||
}
|
||||
return SynchronousFuture<String>('');
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '${describeIdentity(this)}()';
|
||||
}
|
||||
@ -107,7 +104,7 @@ Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize
|
||||
devicePixelRatio: ratio,
|
||||
),
|
||||
child: DefaultAssetBundle(
|
||||
bundle: bundle ?? TestAssetBundle(),
|
||||
bundle: bundle ?? TestAssetBundle(manifest: testManifest),
|
||||
child: Center(
|
||||
child: inferSize ?
|
||||
Image(
|
||||
@ -260,46 +257,21 @@ 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.
|
||||
|
||||
const String manifest = '''
|
||||
final Map<Object?, Object?> manifest = json.decode('''
|
||||
{
|
||||
"assets/image.png" : [
|
||||
"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"
|
||||
{"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}
|
||||
]
|
||||
}
|
||||
''';
|
||||
''') as Map<Object?, Object?>;
|
||||
final AssetBundle bundle = TestAssetBundle(manifest: manifest);
|
||||
|
||||
const double ratio = 1.0;
|
||||
@ -338,14 +310,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: '''
|
||||
final Map<Object?, Object?> manifest = json.decode('''
|
||||
{
|
||||
"assets/image.png" : [
|
||||
"assets/image.png",
|
||||
"assets/1.5x/image.png"
|
||||
{"asset": "assets/1.5x/image.png", "dpr": 1.5}
|
||||
]
|
||||
}
|
||||
''');
|
||||
''') as Map<Object?, Object?>;
|
||||
final AssetBundle bundle = TestAssetBundle(manifest: manifest);
|
||||
|
||||
Future<void> testRatio({required double ratio, required double expectedScale}) async {
|
||||
Key key = GlobalKey();
|
||||
|
Loading…
x
Reference in New Issue
Block a user