Unverified Commit 57c7aa53 authored by Andrew Kolos's avatar Andrew Kolos Committed by GitHub

Reland "Speed up first asset load by using the binary-formatted asset manifest...

Reland "Speed up first asset load by using the binary-formatted asset manifest for image resolution (#121322)

Reland "Speed up first asset load by using the binary-formatted asset manifest for image resolution
parent a7f05473
...@@ -4,15 +4,12 @@ ...@@ -4,15 +4,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'image_provider.dart'; import 'image_provider.dart';
const String _kAssetManifestFileName = 'AssetManifest.json';
/// A screen with a device-pixel ratio strictly less than this value is /// A screen with a device-pixel ratio strictly less than this value is
/// considered a low-resolution screen (typically entry-level to mid-range /// considered a low-resolution screen (typically entry-level to mid-range
/// laptops, desktop screens up to QHD, low-end tablets such as Kindle Fire). /// laptops, desktop screens up to QHD, low-end tablets such as Kindle Fire).
...@@ -284,18 +281,18 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -284,18 +281,18 @@ class AssetImage extends AssetBundleImageProvider {
Completer<AssetBundleImageKey>? completer; Completer<AssetBundleImageKey>? completer;
Future<AssetBundleImageKey>? result; Future<AssetBundleImageKey>? result;
chosenBundle.loadStructuredData<Map<String, List<String>>?>(_kAssetManifestFileName, manifestParser).then<void>( AssetManifest.loadFromAssetBundle(chosenBundle)
(Map<String, List<String>>? manifest) { .then((AssetManifest manifest) {
final String chosenName = _chooseVariant( final Iterable<AssetMetadata>? candidateVariants = manifest.getAssetVariants(keyName);
final AssetMetadata chosenVariant = _chooseVariant(
keyName, keyName,
configuration, configuration,
manifest == null ? null : manifest[keyName], candidateVariants,
)!; );
final double chosenScale = _parseScale(chosenName);
final AssetBundleImageKey key = AssetBundleImageKey( final AssetBundleImageKey key = AssetBundleImageKey(
bundle: chosenBundle, bundle: chosenBundle,
name: chosenName, name: chosenVariant.key,
scale: chosenScale, scale: chosenVariant.targetDevicePixelRatio ?? _naturalResolution,
); );
if (completer != null) { if (completer != null) {
// We already returned from this function, which means we are in the // We already returned from this function, which means we are in the
...@@ -309,14 +306,15 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -309,14 +306,15 @@ class AssetImage extends AssetBundleImageProvider {
// ourselves. // ourselves.
result = SynchronousFuture<AssetBundleImageKey>(key); 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.) // We had an error. (This guarantees we weren't called synchronously.)
// Forward the error to the caller. // Forward the error to the caller.
assert(completer != null); assert(completer != null);
assert(result == null); assert(result == null);
completer!.completeError(error, stack); completer!.completeError(error, stack);
}); });
if (result != null) { if (result != null) {
// The code above ran synchronously, and came up with an answer. // The code above ran synchronously, and came up with an answer.
// Return the SynchronousFuture that we created above. // Return the SynchronousFuture that we created above.
...@@ -328,35 +326,24 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -328,35 +326,24 @@ class AssetImage extends AssetBundleImageProvider {
return completer.future; return completer.future;
} }
/// Parses the asset manifest string into a strongly-typed map. AssetMetadata _chooseVariant(String mainAssetKey, ImageConfiguration config, Iterable<AssetMetadata>? candidateVariants) {
@visibleForTesting if (candidateVariants == null) {
static Future<Map<String, List<String>>?> manifestParser(String? jsonData) { return AssetMetadata(key: mainAssetKey, targetDevicePixelRatio: null, main: true);
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);
} }
String? _chooseVariant(String main, ImageConfiguration config, List<String>? candidates) { if (config.devicePixelRatio == null) {
if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) { return candidateVariants.firstWhere((AssetMetadata variant) => variant.main);
return main;
} }
// TODO(ianh): Consider moving this parsing logic into _manifestParser.
final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>(); final SplayTreeMap<double, AssetMetadata> candidatesByDevicePixelRatio =
for (final String candidate in candidates) { SplayTreeMap<double, AssetMetadata>();
mapping[_parseScale(candidate)] = candidate; for (final AssetMetadata candidate in candidateVariants) {
candidatesByDevicePixelRatio[candidate.targetDevicePixelRatio ?? _naturalResolution] = candidate;
} }
// TODO(ianh): implement support for config.locale, config.textDirection, // TODO(ianh): implement support for config.locale, config.textDirection,
// config.size, config.platform (then document this over in the Image.asset // config.size, config.platform (then document this over in the Image.asset
// docs) // docs)
return _findBestVariant(mapping, config.devicePixelRatio!); return _findBestVariant(candidatesByDevicePixelRatio, config.devicePixelRatio!);
} }
// Returns the "best" asset variant amongst the available `candidates`. // Returns the "best" asset variant amongst the available `candidates`.
...@@ -371,17 +358,17 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -371,17 +358,17 @@ class AssetImage extends AssetBundleImageProvider {
// lowest key higher than `value`. // lowest key higher than `value`.
// - If the screen has high device pixel ratio, choose the variant with the // - If the screen has high device pixel ratio, choose the variant with the
// key nearest to `value`. // key nearest to `value`.
String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) { AssetMetadata _findBestVariant(SplayTreeMap<double, AssetMetadata> candidatesByDpr, double value) {
if (candidates.containsKey(value)) { if (candidatesByDpr.containsKey(value)) {
return candidates[value]!; return candidatesByDpr[value]!;
} }
final double? lower = candidates.lastKeyBefore(value); final double? lower = candidatesByDpr.lastKeyBefore(value);
final double? upper = candidates.firstKeyAfter(value); final double? upper = candidatesByDpr.firstKeyAfter(value);
if (lower == null) { if (lower == null) {
return candidates[upper]; return candidatesByDpr[upper]!;
} }
if (upper == null) { if (upper == null) {
return candidates[lower]; return candidatesByDpr[lower]!;
} }
// On screens with low device-pixel ratios the artifacts from upscaling // On screens with low device-pixel ratios the artifacts from upscaling
...@@ -389,30 +376,10 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -389,30 +376,10 @@ class AssetImage extends AssetBundleImageProvider {
// ratios because the physical pixels are larger. Choose the higher // ratios because the physical pixels are larger. Choose the higher
// resolution image in that case instead of the nearest one. // resolution image in that case instead of the nearest one.
if (value < _kLowDprLimit || value > (lower + upper) / 2) { if (value < _kLowDprLimit || value > (lower + upper) / 2) {
return candidates[upper]; return candidatesByDpr[upper]!;
} else { } 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 @override
......
...@@ -30,14 +30,12 @@ abstract class AssetManifest { ...@@ -30,14 +30,12 @@ abstract class AssetManifest {
/// information. /// information.
List<String> listAssets(); 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 /// This method considers a main asset to be a variant of itself and
/// includes it in the returned list. /// includes it in the returned list.
/// List<AssetMetadata>? getAssetVariants(String key);
/// 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 // Lazily parses the binary asset manifest into a data structure that's easier to work
...@@ -64,14 +62,14 @@ class _AssetManifestBin implements AssetManifest { ...@@ -64,14 +62,14 @@ class _AssetManifestBin implements AssetManifest {
final Map<String, List<AssetMetadata>> _typeCastedData = <String, List<AssetMetadata>>{}; final Map<String, List<AssetMetadata>> _typeCastedData = <String, List<AssetMetadata>>{};
@override @override
List<AssetMetadata> getAssetVariants(String key) { List<AssetMetadata>? getAssetVariants(String key) {
// We lazily delay typecasting to prevent a performance hiccup when parsing // We lazily delay typecasting to prevent a performance hiccup when parsing
// large asset manifests. This is important to keep an app's first asset // large asset manifests. This is important to keep an app's first asset
// load fast. // load fast.
if (!_typeCastedData.containsKey(key)) { if (!_typeCastedData.containsKey(key)) {
final Object? variantData = _data[key]; final Object? variantData = _data[key];
if (variantData == null) { 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?>) _typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
.cast<Map<Object?, Object?>>() .cast<Map<Object?, Object?>>()
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:convert';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -13,18 +12,14 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -13,18 +12,14 @@ import 'package:flutter_test/flutter_test.dart';
class TestAssetBundle extends CachingAssetBundle { class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle(this._assetBundleMap); TestAssetBundle(this._assetBundleMap);
final Map<String, List<String>> _assetBundleMap; final Map<String, List<Map<Object?, Object?>>> _assetBundleMap;
Map<String, int> loadCallCount = <String, int>{}; Map<String, int> loadCallCount = <String, int>{};
String get _assetBundleContents {
return json.encode(_assetBundleMap);
}
@override @override
Future<ByteData> load(String key) async { Future<ByteData> load(String key) async {
if (key == 'AssetManifest.json') { if (key == 'AssetManifest.bin') {
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(_assetBundleContents)).buffer); return const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
} }
loadCallCount[key] = loadCallCount[key] ?? 0 + 1; loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
...@@ -45,9 +40,10 @@ class TestAssetBundle extends CachingAssetBundle { ...@@ -45,9 +40,10 @@ class TestAssetBundle extends CachingAssetBundle {
void main() { void main() {
group('1.0 scale device tests', () { group('1.0 scale device tests', () {
void buildAndTestWithOneAsset(String mainAssetPath) { 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( final AssetImage assetImage = AssetImage(
mainAssetPath, mainAssetPath,
...@@ -93,11 +89,13 @@ void main() { ...@@ -93,11 +89,13 @@ void main() {
const String mainAssetPath = 'assets/normalFolder/normalFile.png'; const String mainAssetPath = 'assets/normalFolder/normalFile.png';
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png'; const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
final Map<String, List<String>> assetBundleMap = final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
<String, List<String>>{}; <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); final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
final AssetImage assetImage = AssetImage( final AssetImage assetImage = AssetImage(
...@@ -123,10 +121,10 @@ void main() { ...@@ -123,10 +121,10 @@ void main() {
test('When high-res device and high-res asset not present in bundle then return main variant', () { test('When high-res device and high-res asset not present in bundle then return main variant', () {
const String mainAssetPath = 'assets/normalFolder/normalFile.png'; const String mainAssetPath = 'assets/normalFolder/normalFile.png';
final Map<String, List<String>> assetBundleMap = final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
<String, List<String>>{}; <String, List<Map<Object?, Object?>>>{};
assetBundleMap[mainAssetPath] = <String>[mainAssetPath]; assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap); final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
...@@ -162,10 +160,13 @@ void main() { ...@@ -162,10 +160,13 @@ void main() {
double chosenAssetRatio, double chosenAssetRatio,
String expectedAssetPath, String expectedAssetPath,
) { ) {
final Map<String, List<String>> assetBundleMap = final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
<String, List<String>>{}; <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); final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
......
...@@ -41,7 +41,7 @@ void main() { ...@@ -41,7 +41,7 @@ void main() {
expect(manifest.listAssets(), unorderedEquals(<String>['assets/foo.png', 'assets/bar.png'])); 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); expect(fooVariants.length, 2);
final AssetMetadata firstFooVariant = fooVariants[0]; final AssetMetadata firstFooVariant = fooVariants[0];
expect(firstFooVariant.key, 'assets/foo.png'); expect(firstFooVariant.key, 'assets/foo.png');
...@@ -52,7 +52,7 @@ void main() { ...@@ -52,7 +52,7 @@ void main() {
expect(secondFooVariant.targetDevicePixelRatio, 2.0); expect(secondFooVariant.targetDevicePixelRatio, 2.0);
expect(secondFooVariant.main, false); 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); expect(barVariants.length, 1);
final AssetMetadata firstBarVariant = barVariants[0]; final AssetMetadata firstBarVariant = barVariants[0];
expect(firstBarVariant.key, 'assets/bar.png'); expect(firstBarVariant.key, 'assets/bar.png');
...@@ -60,9 +60,8 @@ void main() { ...@@ -60,9 +60,8 @@ void main() {
expect(firstBarVariant.main, true); 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()); final AssetManifest manifest = await AssetManifest.loadFromAssetBundle(TestAssetBundle());
expect(manifest.getAssetVariants('invalid asset key'), isNull);
expect(() => manifest.getAssetVariants('invalid asset key'), throwsArgumentError);
}); });
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
@TestOn('!chrome') @TestOn('!chrome')
library; library;
import 'dart:convert';
import 'dart:ui' as ui show Image; import 'dart:ui' as ui show Image;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -18,27 +19,31 @@ import '../image_data.dart'; ...@@ -18,27 +19,31 @@ import '../image_data.dart';
ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale); ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
double scaleOf(ByteData data) => data.getFloat64(0); double scaleOf(ByteData data) => data.getFloat64(0);
const String testManifest = ''' final Map<Object?, Object?> testManifest = json.decode('''
{ {
"assets/image.png" : [ "assets/image.png" : [
"assets/image.png", {"asset": "assets/1.5x/image.png", "dpr": 1.5},
"assets/1.5x/image.png", {"asset": "assets/2.0x/image.png", "dpr": 2.0},
"assets/2.0x/image.png", {"asset": "assets/3.0x/image.png", "dpr": 3.0},
"assets/3.0x/image.png", {"asset": "assets/4.0x/image.png", "dpr": 4.0}
"assets/4.0x/image.png"
] ]
} }
'''; ''') as Map<Object?, Object?>;
class TestAssetBundle extends CachingAssetBundle { 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 @override
Future<ByteData> load(String key) { Future<ByteData> load(String key) {
late ByteData data; late ByteData data;
switch (key) { switch (key) {
case 'AssetManifest.bin':
data = manifest;
break;
case 'assets/image.png': case 'assets/image.png':
data = testByteData(1.0); data = testByteData(1.0);
break; break;
...@@ -61,14 +66,6 @@ class TestAssetBundle extends CachingAssetBundle { ...@@ -61,14 +66,6 @@ class TestAssetBundle extends CachingAssetBundle {
return SynchronousFuture<ByteData>(data); 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 @override
String toString() => '${describeIdentity(this)}()'; String toString() => '${describeIdentity(this)}()';
} }
...@@ -107,7 +104,7 @@ Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize ...@@ -107,7 +104,7 @@ Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize
devicePixelRatio: ratio, devicePixelRatio: ratio,
), ),
child: DefaultAssetBundle( child: DefaultAssetBundle(
bundle: bundle ?? TestAssetBundle(), bundle: bundle ?? TestAssetBundle(manifest: testManifest),
child: Center( child: Center(
child: inferSize ? child: inferSize ?
Image( Image(
...@@ -260,46 +257,21 @@ void main() { ...@@ -260,46 +257,21 @@ void main() {
expect(getRenderImage(tester, key).scale, 4.0); 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 { 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 // If both a main asset and a 1.0x asset are specified, then prefer
// the 1.0x asset. // the 1.0x asset.
const String manifest = ''' final Map<Object?, Object?> manifest = json.decode('''
{ {
"assets/image.png" : [ "assets/image.png" : [
"assets/image.png", {"asset": "assets/1.0x/image.png", "dpr": 1.0},
"assets/1.0x/image.png", {"asset": "assets/1.5x/image.png", "dpr": 1.5},
"assets/1.5x/image.png", {"asset": "assets/2.0x/image.png", "dpr": 2.0},
"assets/2.0x/image.png", {"asset": "assets/3.0x/image.png", "dpr": 3.0},
"assets/3.0x/image.png", {"asset": "assets/4.0x/image.png", "dpr": 4.0}
"assets/4.0x/image.png"
] ]
} }
'''; ''') as Map<Object?, Object?>;
final AssetBundle bundle = TestAssetBundle(manifest: manifest); final AssetBundle bundle = TestAssetBundle(manifest: manifest);
const double ratio = 1.0; const double ratio = 1.0;
...@@ -338,14 +310,14 @@ void main() { ...@@ -338,14 +310,14 @@ void main() {
// if higher resolution assets are not available we will pick the best // if higher resolution assets are not available we will pick the best
// available. // available.
testWidgets('Low-resolution assets', (WidgetTester tester) async { 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/image.png", {"asset": "assets/1.5x/image.png", "dpr": 1.5}
"assets/1.5x/image.png"
] ]
} }
'''); ''') as Map<Object?, Object?>;
final AssetBundle bundle = TestAssetBundle(manifest: manifest);
Future<void> testRatio({required double ratio, required double expectedScale}) async { Future<void> testRatio({required double ratio, required double expectedScale}) async {
Key key = GlobalKey(); Key key = GlobalKey();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment