Separate web and io implementations of network image (#34112)
* add web and io implemenations of network and asset image * fix foundation import * update to remove extra asset image indirection * skip chunk test * address comments * disable non-functional test * disable all golden tests * address comments
This commit is contained in:
parent
fd1291fe5e
commit
dfa39f3ee5
124
packages/flutter/lib/src/painting/_network_image_io.dart
Normal file
124
packages/flutter/lib/src/painting/_network_image_io.dart
Normal file
@ -0,0 +1,124 @@
|
||||
// Copyright 2019 The Chromium 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 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'binding.dart';
|
||||
import 'debug.dart';
|
||||
import 'image_provider.dart' as image_provider;
|
||||
import 'image_stream.dart';
|
||||
|
||||
/// The dart:io implemenation of [image_provider.NetworkImage].
|
||||
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {
|
||||
/// Creates an object that fetches the image at the given URL.
|
||||
///
|
||||
/// The arguments [url] and [scale] must not be null.
|
||||
const NetworkImage(this.url, { this.scale = 1.0, this.headers })
|
||||
: assert(url != null),
|
||||
assert(scale != null);
|
||||
|
||||
@override
|
||||
final String url;
|
||||
|
||||
@override
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
final Map<String, String> headers;
|
||||
|
||||
@override
|
||||
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
|
||||
return SynchronousFuture<NetworkImage>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(image_provider.NetworkImage key) {
|
||||
// Ownership of this controller is handed off to [_loadAsync]; it is that
|
||||
// method's responsibility to close the controller's stream when the image
|
||||
// has been loaded or an error is thrown.
|
||||
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
|
||||
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, chunkEvents),
|
||||
chunkEvents: chunkEvents.stream,
|
||||
scale: key.scale,
|
||||
informationCollector: () {
|
||||
return <DiagnosticsNode>[
|
||||
DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Do not access this field directly; use [_httpClient] instead.
|
||||
// We set `autoUncompress` to false to ensure that we can trust the value of
|
||||
// the `Content-Length` HTTP header. We automatically uncompress the content
|
||||
// in our call to [consolidateHttpClientResponseBytes].
|
||||
static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;
|
||||
|
||||
static HttpClient get _httpClient {
|
||||
HttpClient client = _sharedHttpClient;
|
||||
assert(() {
|
||||
if (debugNetworkImageHttpClientProvider != null)
|
||||
client = debugNetworkImageHttpClientProvider();
|
||||
return true;
|
||||
}());
|
||||
return client;
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(
|
||||
NetworkImage key,
|
||||
StreamController<ImageChunkEvent> chunkEvents,
|
||||
) async {
|
||||
try {
|
||||
assert(key == this);
|
||||
|
||||
final Uri resolved = Uri.base.resolve(key.url);
|
||||
final HttpClientRequest request = await _httpClient.getUrl(resolved);
|
||||
headers?.forEach((String name, String value) {
|
||||
request.headers.add(name, value);
|
||||
});
|
||||
final HttpClientResponse response = await request.close();
|
||||
if (response.statusCode != HttpStatus.ok)
|
||||
throw Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');
|
||||
|
||||
final Uint8List bytes = await consolidateHttpClientResponseBytes(
|
||||
response,
|
||||
onBytesReceived: (int cumulative, int total) {
|
||||
chunkEvents.add(ImageChunkEvent(
|
||||
cumulativeBytesLoaded: cumulative,
|
||||
expectedTotalBytes: total,
|
||||
));
|
||||
},
|
||||
);
|
||||
if (bytes.lengthInBytes == 0)
|
||||
throw Exception('NetworkImage is an empty file: $resolved');
|
||||
|
||||
return PaintingBinding.instance.instantiateImageCodec(bytes);
|
||||
} finally {
|
||||
chunkEvents.close();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other.runtimeType != runtimeType)
|
||||
return false;
|
||||
final NetworkImage typedOther = other;
|
||||
return url == typedOther.url
|
||||
&& scale == typedOther.scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => ui.hashValues(url, scale);
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType("$url", scale: $scale)';
|
||||
}
|
73
packages/flutter/lib/src/painting/_network_image_web.dart
Normal file
73
packages/flutter/lib/src/painting/_network_image_web.dart
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright 2019 The Chromium 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 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'image_provider.dart' as image_provider;
|
||||
import 'image_stream.dart';
|
||||
|
||||
/// The dart:html implemenation of [image_provider.NetworkImage].
|
||||
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {
|
||||
/// Creates an object that fetches the image at the given URL.
|
||||
///
|
||||
/// The arguments [url] and [scale] must not be null.
|
||||
const NetworkImage(this.url, {this.scale = 1.0, this.headers})
|
||||
: assert(url != null),
|
||||
assert(scale != null);
|
||||
|
||||
@override
|
||||
final String url;
|
||||
|
||||
@override
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
final Map<String, String> headers;
|
||||
|
||||
@override
|
||||
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
|
||||
return SynchronousFuture<NetworkImage>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(image_provider.NetworkImage key) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key),
|
||||
scale: key.scale,
|
||||
informationCollector: () {
|
||||
return <DiagnosticsNode>[
|
||||
DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<NetworkImage>('Image key', key),
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(NetworkImage key) async {
|
||||
assert(key == this);
|
||||
|
||||
final Uri resolved = Uri.base.resolve(key.url);
|
||||
// This API only exists in the web engine implementation and is not
|
||||
// contained in the analyzer summary for Flutter.
|
||||
return ui.webOnlyInstantiateImageCodecFromUrl(resolved); // ignore: undefined_function
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
final NetworkImage typedOther = other;
|
||||
return url == typedOther.url && scale == typedOther.scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => ui.hashValues(url, scale);
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType("$url", scale: $scale)';
|
||||
}
|
@ -11,8 +11,9 @@ import 'dart:ui' show Size, Locale, TextDirection, hashValues;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '_network_image_io.dart'
|
||||
if (dart.library.html) '_network_image_web.dart' as network_image;
|
||||
import 'binding.dart';
|
||||
import 'debug.dart';
|
||||
import 'image_cache.dart';
|
||||
import 'image_stream.dart';
|
||||
|
||||
@ -478,110 +479,25 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe
|
||||
// TODO(ianh): Find some way to honor cache headers to the extent that when the
|
||||
// last reference to an image is released, we proactively evict the image from
|
||||
// our cache if the headers describe the image as having expired at that point.
|
||||
class NetworkImage extends ImageProvider<NetworkImage> {
|
||||
abstract class NetworkImage extends ImageProvider<NetworkImage> {
|
||||
/// Creates an object that fetches the image at the given URL.
|
||||
///
|
||||
/// The arguments must not be null.
|
||||
const NetworkImage(this.url, { this.scale = 1.0, this.headers })
|
||||
: assert(url != null),
|
||||
assert(scale != null);
|
||||
/// The arguments [url] and [scale] must not be null.
|
||||
const factory NetworkImage(String url, { double scale, Map<String, String> headers }) = network_image.NetworkImage;
|
||||
|
||||
/// The URL from which the image will be fetched.
|
||||
final String url;
|
||||
String get url;
|
||||
|
||||
/// The scale to place in the [ImageInfo] object of the image.
|
||||
final double scale;
|
||||
double get scale;
|
||||
|
||||
/// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
|
||||
final Map<String, String> headers;
|
||||
///
|
||||
/// When running flutter on the web, headers are not used.
|
||||
Map<String, String> get headers;
|
||||
|
||||
@override
|
||||
Future<NetworkImage> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<NetworkImage>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(NetworkImage key) {
|
||||
// Ownership of this controller is handed off to [_loadAsync]; it is that
|
||||
// method's responsibility to close the controller's stream when the image
|
||||
// has been loaded or an error is thrown.
|
||||
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
|
||||
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, chunkEvents),
|
||||
chunkEvents: chunkEvents.stream,
|
||||
scale: key.scale,
|
||||
informationCollector: () sync* {
|
||||
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
|
||||
yield DiagnosticsProperty<NetworkImage>('Image key', key);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Do not access this field directly; use [_httpClient] instead.
|
||||
// We set `autoUncompress` to false to ensure that we can trust the value of
|
||||
// the `Content-Length` HTTP header. We automatically uncompress the content
|
||||
// in our call to [consolidateHttpClientResponseBytes].
|
||||
static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;
|
||||
|
||||
static HttpClient get _httpClient {
|
||||
HttpClient client = _sharedHttpClient;
|
||||
assert(() {
|
||||
if (debugNetworkImageHttpClientProvider != null)
|
||||
client = debugNetworkImageHttpClientProvider();
|
||||
return true;
|
||||
}());
|
||||
return client;
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(
|
||||
NetworkImage key,
|
||||
StreamController<ImageChunkEvent> chunkEvents,
|
||||
) async {
|
||||
try {
|
||||
assert(key == this);
|
||||
|
||||
final Uri resolved = Uri.base.resolve(key.url);
|
||||
final HttpClientRequest request = await _httpClient.getUrl(resolved);
|
||||
headers?.forEach((String name, String value) {
|
||||
request.headers.add(name, value);
|
||||
});
|
||||
final HttpClientResponse response = await request.close();
|
||||
if (response.statusCode != HttpStatus.ok)
|
||||
throw Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved');
|
||||
|
||||
final Uint8List bytes = await consolidateHttpClientResponseBytes(
|
||||
response,
|
||||
onBytesReceived: (int cumulative, int total) {
|
||||
chunkEvents.add(ImageChunkEvent(
|
||||
cumulativeBytesLoaded: cumulative,
|
||||
expectedTotalBytes: total,
|
||||
));
|
||||
},
|
||||
);
|
||||
if (bytes.lengthInBytes == 0)
|
||||
throw Exception('NetworkImage is an empty file: $resolved');
|
||||
|
||||
return PaintingBinding.instance.instantiateImageCodec(bytes);
|
||||
} finally {
|
||||
chunkEvents.close();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other.runtimeType != runtimeType)
|
||||
return false;
|
||||
final NetworkImage typedOther = other;
|
||||
return url == typedOther.url
|
||||
&& scale == typedOther.scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(url, scale);
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType("$url", scale: $scale)';
|
||||
ImageStreamCompleter load(NetworkImage key);
|
||||
}
|
||||
|
||||
/// Decodes the given [File] object as an image, associating it with the given
|
||||
|
@ -5,7 +5,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' show hashValues;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -265,10 +264,13 @@ class AssetImage extends AssetBundleImageProvider {
|
||||
return _naturalResolution;
|
||||
}
|
||||
|
||||
final File assetPath = File(key);
|
||||
final Directory assetDir = assetPath.parent;
|
||||
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(assetDir.path);
|
||||
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
|
||||
|
@ -253,7 +253,7 @@ void main() {
|
||||
expect(events[i].cumulativeBytesLoaded, math.min((i + 1) * chunkSize, kTransparentImage.length));
|
||||
expect(events[i].expectedTotalBytes, kTransparentImage.length);
|
||||
}
|
||||
});
|
||||
}, skip: isBrowser);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
|
||||
class TestAssetBundle extends CachingAssetBundle {
|
||||
TestAssetBundle(this._assetBundleMap);
|
||||
|
||||
|
@ -41,7 +41,7 @@ void main() {
|
||||
});
|
||||
return client;
|
||||
});
|
||||
});
|
||||
}, skip: isBrowser);
|
||||
}
|
||||
|
||||
class MockHttpClient extends Mock implements HttpClient {}
|
||||
|
Loading…
x
Reference in New Issue
Block a user