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/foundation.dart';
|
||||||
import 'package:flutter/services.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 'binding.dart';
|
||||||
import 'debug.dart';
|
|
||||||
import 'image_cache.dart';
|
import 'image_cache.dart';
|
||||||
import 'image_stream.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
|
// 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
|
// 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.
|
// 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.
|
/// Creates an object that fetches the image at the given URL.
|
||||||
///
|
///
|
||||||
/// The arguments must not be null.
|
/// The arguments [url] and [scale] must not be null.
|
||||||
const NetworkImage(this.url, { this.scale = 1.0, this.headers })
|
const factory NetworkImage(String url, { double scale, Map<String, String> headers }) = network_image.NetworkImage;
|
||||||
: assert(url != null),
|
|
||||||
assert(scale != null);
|
|
||||||
|
|
||||||
/// The URL from which the image will be fetched.
|
/// 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.
|
/// 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.
|
/// 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
|
@override
|
||||||
Future<NetworkImage> obtainKey(ImageConfiguration configuration) {
|
ImageStreamCompleter load(NetworkImage key);
|
||||||
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)';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes the given [File] object as an image, associating it with the given
|
/// Decodes the given [File] object as an image, associating it with the given
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:ui' show hashValues;
|
import 'dart:ui' show hashValues;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@ -265,10 +264,13 @@ class AssetImage extends AssetBundleImageProvider {
|
|||||||
return _naturalResolution;
|
return _naturalResolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
final File assetPath = File(key);
|
final Uri assetUri = Uri.parse(key);
|
||||||
final Directory assetDir = assetPath.parent;
|
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)
|
if (match != null && match.groupCount > 0)
|
||||||
return double.parse(match.group(1));
|
return double.parse(match.group(1));
|
||||||
return _naturalResolution; // i.e. default to 1.0x
|
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].cumulativeBytesLoaded, math.min((i + 1) * chunkSize, kTransparentImage.length));
|
||||||
expect(events[i].expectedTotalBytes, 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/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
|
||||||
class TestAssetBundle extends CachingAssetBundle {
|
class TestAssetBundle extends CachingAssetBundle {
|
||||||
TestAssetBundle(this._assetBundleMap);
|
TestAssetBundle(this._assetBundleMap);
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
return client;
|
return client;
|
||||||
});
|
});
|
||||||
});
|
}, skip: isBrowser);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockHttpClient extends Mock implements HttpClient {}
|
class MockHttpClient extends Mock implements HttpClient {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user