Sample code for ImageProvider (#131952)
Also: - minor improvements to documentation - wrap one of our test error messages in a manner more consistent with other messages
This commit is contained in:
parent
702b78c6be
commit
9c8f3950e3
@ -1006,14 +1006,11 @@ Future<void> _runFrameworkTests() async {
|
|||||||
await _runFlutterTest(path.join(flutterRoot, 'packages', 'fuchsia_remote_debug_protocol'));
|
await _runFlutterTest(path.join(flutterRoot, 'packages', 'fuchsia_remote_debug_protocol'));
|
||||||
await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'non_nullable'));
|
await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'non_nullable'));
|
||||||
const String httpClientWarning =
|
const String httpClientWarning =
|
||||||
'Warning: At least one test in this suite creates an HttpClient. When\n'
|
'Warning: At least one test in this suite creates an HttpClient. When running a test suite that uses\n'
|
||||||
'running a test suite that uses TestWidgetsFlutterBinding, all HTTP\n'
|
'TestWidgetsFlutterBinding, all HTTP requests will return status code 400, and no network request\n'
|
||||||
'requests will return status code 400, and no network request will\n'
|
'will actually be made. Any test expecting a real network connection and status code will fail.\n'
|
||||||
'actually be made. Any test expecting a real network connection and\n'
|
'To test code that needs an HttpClient, provide your own HttpClient implementation to the code under\n'
|
||||||
'status code will fail.\n'
|
'test, so that your test can consistently provide a testable response to the code under test.';
|
||||||
'To test code that needs an HttpClient, provide your own HttpClient\n'
|
|
||||||
'implementation to the code under test, so that your test can\n'
|
|
||||||
'consistently provide a testable response to the code under test.';
|
|
||||||
await _runFlutterTest(
|
await _runFlutterTest(
|
||||||
path.join(flutterRoot, 'packages', 'flutter_test'),
|
path.join(flutterRoot, 'packages', 'flutter_test'),
|
||||||
script: path.join('test', 'bindings_test_failure.dart'),
|
script: path.join('test', 'bindings_test_failure.dart'),
|
||||||
|
104
examples/api/lib/painting/image_provider/image_provider.0.dart
Normal file
104
examples/api/lib/painting/image_provider/image_provider.0.dart
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// 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 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class CustomNetworkImage extends ImageProvider<Uri> {
|
||||||
|
const CustomNetworkImage(this.url);
|
||||||
|
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uri> obtainKey(ImageConfiguration configuration) {
|
||||||
|
final Uri result = Uri.parse(url).replace(
|
||||||
|
queryParameters: <String, String>{
|
||||||
|
'dpr': '${configuration.devicePixelRatio}',
|
||||||
|
'locale': '${configuration.locale?.toLanguageTag()}',
|
||||||
|
'platform': '${configuration.platform?.name}',
|
||||||
|
'width': '${configuration.size?.width}',
|
||||||
|
'height': '${configuration.size?.height}',
|
||||||
|
'bidi': '${configuration.textDirection?.name}',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return SynchronousFuture<Uri>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static HttpClient get _httpClient {
|
||||||
|
HttpClient? client;
|
||||||
|
assert(() {
|
||||||
|
if (debugNetworkImageHttpClientProvider != null) {
|
||||||
|
client = debugNetworkImageHttpClientProvider!();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
return client ?? HttpClient()..autoUncompress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter loadImage(Uri key, ImageDecoderCallback decode) {
|
||||||
|
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
|
||||||
|
debugPrint('Fetching "$key"...');
|
||||||
|
return MultiFrameImageStreamCompleter(
|
||||||
|
codec: _httpClient.getUrl(key)
|
||||||
|
.then<HttpClientResponse>((HttpClientRequest request) => request.close())
|
||||||
|
.then<Uint8List>((HttpClientResponse response) {
|
||||||
|
return consolidateHttpClientResponseBytes(
|
||||||
|
response,
|
||||||
|
onBytesReceived: (int cumulative, int? total) {
|
||||||
|
chunkEvents.add(ImageChunkEvent(
|
||||||
|
cumulativeBytesLoaded: cumulative,
|
||||||
|
expectedTotalBytes: total,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catchError((Object e, StackTrace stack) {
|
||||||
|
scheduleMicrotask(() {
|
||||||
|
PaintingBinding.instance.imageCache.evict(key);
|
||||||
|
});
|
||||||
|
return Future<Uint8List>.error(e, stack);
|
||||||
|
})
|
||||||
|
.whenComplete(chunkEvents.close)
|
||||||
|
.then<ui.ImmutableBuffer>(ui.ImmutableBuffer.fromUint8List)
|
||||||
|
.then<ui.Codec>(decode),
|
||||||
|
chunkEvents: chunkEvents.stream,
|
||||||
|
scale: 1.0,
|
||||||
|
debugLabel: '"key"',
|
||||||
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
|
DiagnosticsProperty<Uri>('URL', key),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '${objectRuntimeType(this, 'CustomNetworkImage')}("$url")';
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() => runApp(const ExampleApp());
|
||||||
|
|
||||||
|
class ExampleApp extends StatelessWidget {
|
||||||
|
const ExampleApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
return Image(
|
||||||
|
image: const CustomNetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/flamingos.jpg'),
|
||||||
|
width: constraints.hasBoundedWidth ? constraints.maxWidth : null,
|
||||||
|
height: constraints.hasBoundedHeight ? constraints.maxHeight : null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
// 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 'package:flutter_api_samples/painting/image_provider/image_provider.0.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('$CustomNetworkImage', (WidgetTester tester) async {
|
||||||
|
const String expectedUrl = 'https://flutter.github.io/assets-for-api-docs/assets/widgets/flamingos.jpg?dpr=3.0&locale=en-US&platform=android&width=800.0&height=600.0&bidi=ltr';
|
||||||
|
final List<String> log = <String>[];
|
||||||
|
final DebugPrintCallback originalDebugPrint = debugPrint;
|
||||||
|
debugPrint = (String? message, {int? wrapWidth}) { log.add('$message'); };
|
||||||
|
await tester.pumpWidget(const ExampleApp());
|
||||||
|
expect(tester.takeException().toString(), 'Exception: Invalid image data');
|
||||||
|
expect(log, <String>['Fetching "$expectedUrl"...']);
|
||||||
|
debugPrint = originalDebugPrint;
|
||||||
|
});
|
||||||
|
}
|
@ -34,6 +34,7 @@ typedef DebugPrintCallback = void Function(String? message, { int? wrapWidth });
|
|||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [DebugPrintCallback], for function parameters and usage details.
|
/// * [DebugPrintCallback], for function parameters and usage details.
|
||||||
|
/// * [debugPrintThrottled], the default implementation.
|
||||||
DebugPrintCallback debugPrint = debugPrintThrottled;
|
DebugPrintCallback debugPrint = debugPrintThrottled;
|
||||||
|
|
||||||
/// Alternative implementation of [debugPrint] that does not throttle.
|
/// Alternative implementation of [debugPrint] that does not throttle.
|
||||||
@ -48,6 +49,8 @@ void debugPrintSynchronously(String? message, { int? wrapWidth }) {
|
|||||||
|
|
||||||
/// Implementation of [debugPrint] that throttles messages. This avoids dropping
|
/// Implementation of [debugPrint] that throttles messages. This avoids dropping
|
||||||
/// messages on platforms that rate-limit their logging (for example, Android).
|
/// messages on platforms that rate-limit their logging (for example, Android).
|
||||||
|
///
|
||||||
|
/// If `wrapWidth` is not null, the message is wrapped using [debugWordWrap].
|
||||||
void debugPrintThrottled(String? message, { int? wrapWidth }) {
|
void debugPrintThrottled(String? message, { int? wrapWidth }) {
|
||||||
final List<String> messageLines = message?.split('\n') ?? <String>['null'];
|
final List<String> messageLines = message?.split('\n') ?? <String>['null'];
|
||||||
if (wrapWidth != null) {
|
if (wrapWidth != null) {
|
||||||
@ -100,6 +103,9 @@ enum _WordWrapParseMode { inSpace, inWord, atBreak }
|
|||||||
|
|
||||||
/// Wraps the given string at the given width.
|
/// Wraps the given string at the given width.
|
||||||
///
|
///
|
||||||
|
/// The `message` should not contain newlines (`\n`, U+000A). Strings that may
|
||||||
|
/// contain newlines should be [String.split] before being wrapped.
|
||||||
|
///
|
||||||
/// Wrapping occurs at space characters (U+0020). Lines that start with an
|
/// Wrapping occurs at space characters (U+0020). Lines that start with an
|
||||||
/// octothorpe ("#", U+0023) are not wrapped (so for example, Dart stack traces
|
/// octothorpe ("#", U+0023) are not wrapped (so for example, Dart stack traces
|
||||||
/// won't be wrapped).
|
/// won't be wrapped).
|
||||||
|
@ -344,6 +344,16 @@ typedef ImageDecoderCallback = Future<ui.Codec> Function(
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// ## Creating an [ImageProvider]
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// In this example, a variant of [NetworkImage] is created that passes all the
|
||||||
|
/// [ImageConfiguration] information (locale, platform, size, etc) to the server
|
||||||
|
/// using query arguments in the image URL.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/painting/image_provider/image_provider.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
abstract class ImageProvider<T extends Object> {
|
abstract class ImageProvider<T extends Object> {
|
||||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||||
@ -596,7 +606,7 @@ abstract class ImageProvider<T extends Object> {
|
|||||||
return cache.evict(key);
|
return cache.evict(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts an ImageProvider's settings plus an ImageConfiguration to a key
|
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
||||||
/// that describes the precise image to load.
|
/// that describes the precise image to load.
|
||||||
///
|
///
|
||||||
/// The type of the key is determined by the subclass. It is a value that
|
/// The type of the key is determined by the subclass. It is a value that
|
||||||
@ -605,6 +615,10 @@ abstract class ImageProvider<T extends Object> {
|
|||||||
/// arguments and [ImageConfiguration] objects should return keys that are
|
/// arguments and [ImageConfiguration] objects should return keys that are
|
||||||
/// '==' to each other (possibly by using a class for the key that itself
|
/// '==' to each other (possibly by using a class for the key that itself
|
||||||
/// implements [==]).
|
/// implements [==]).
|
||||||
|
///
|
||||||
|
/// If the result can be determined synchronously, this function should return
|
||||||
|
/// a [SynchronousFuture]. This allows image resolution to progress
|
||||||
|
/// synchronously during a frame rather than delaying image loading.
|
||||||
Future<T> obtainKey(ImageConfiguration configuration);
|
Future<T> obtainKey(ImageConfiguration configuration);
|
||||||
|
|
||||||
/// Converts a key into an [ImageStreamCompleter], and begins fetching the
|
/// Converts a key into an [ImageStreamCompleter], and begins fetching the
|
||||||
@ -632,10 +646,7 @@ abstract class ImageProvider<T extends Object> {
|
|||||||
/// Converts a key into an [ImageStreamCompleter], and begins fetching the
|
/// Converts a key into an [ImageStreamCompleter], and begins fetching the
|
||||||
/// image.
|
/// image.
|
||||||
///
|
///
|
||||||
/// For backwards-compatibility the default implementation of this method returns
|
/// This method is deprecated. Implement [loadImage] instead.
|
||||||
/// an object that will cause [resolveStreamForKey] to consult [load]. However,
|
|
||||||
/// implementors of this interface should only override this method and not
|
|
||||||
/// [load], which is deprecated.
|
|
||||||
///
|
///
|
||||||
/// The [decode] callback provides the logic to obtain the codec for the
|
/// The [decode] callback provides the logic to obtain the codec for the
|
||||||
/// image.
|
/// image.
|
||||||
@ -1477,6 +1488,8 @@ class ResizeImage extends ImageProvider<ResizeImageKey> {
|
|||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage].
|
/// * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage].
|
||||||
|
/// * The example at [ImageProvider], which shows a custom variant of this class
|
||||||
|
/// that applies different logic for fetching the image.
|
||||||
// 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.
|
||||||
@ -1494,7 +1507,7 @@ abstract class NetworkImage extends ImageProvider<NetworkImage> {
|
|||||||
|
|
||||||
/// 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.
|
||||||
///
|
///
|
||||||
/// When running flutter on the web, headers are not used.
|
/// When running Flutter on the web, headers are not used.
|
||||||
Map<String, String>? get headers;
|
Map<String, String>? get headers;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -309,6 +309,16 @@ typedef ImageErrorWidgetBuilder = Widget Function(
|
|||||||
/// using the HTML renderer, the web engine delegates image decoding of network
|
/// using the HTML renderer, the web engine delegates image decoding of network
|
||||||
/// images to the Web, which does not support custom decode sizes.
|
/// images to the Web, which does not support custom decode sizes.
|
||||||
///
|
///
|
||||||
|
/// ## Custom image providers
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// In this example, a variant of [NetworkImage] is created that passes all the
|
||||||
|
/// [ImageConfiguration] information (locale, platform, size, etc) to the server
|
||||||
|
/// using query arguments in the image URL.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/painting/image_provider/image_provider.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [Icon], which shows an image from a font.
|
/// * [Icon], which shows an image from a font.
|
||||||
@ -819,7 +829,7 @@ class Image extends StatefulWidget {
|
|||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
final ImageErrorWidgetBuilder? errorBuilder;
|
final ImageErrorWidgetBuilder? errorBuilder;
|
||||||
|
|
||||||
/// If non-null, require the image to have this width.
|
/// If non-null, require the image to have this width (in logical pixels).
|
||||||
///
|
///
|
||||||
/// If null, the image will pick a size that best preserves its intrinsic
|
/// If null, the image will pick a size that best preserves its intrinsic
|
||||||
/// aspect ratio.
|
/// aspect ratio.
|
||||||
@ -831,7 +841,7 @@ class Image extends StatefulWidget {
|
|||||||
/// and height if the exact image dimensions are not known in advance.
|
/// and height if the exact image dimensions are not known in advance.
|
||||||
final double? width;
|
final double? width;
|
||||||
|
|
||||||
/// If non-null, require the image to have this height.
|
/// If non-null, require the image to have this height (in logical pixels).
|
||||||
///
|
///
|
||||||
/// If null, the image will pick a size that best preserves its intrinsic
|
/// If null, the image will pick a size that best preserves its intrinsic
|
||||||
/// aspect ratio.
|
/// aspect ratio.
|
||||||
|
@ -71,17 +71,21 @@ void mockFlutterAssets() {
|
|||||||
class _MockHttpOverrides extends HttpOverrides {
|
class _MockHttpOverrides extends HttpOverrides {
|
||||||
bool warningPrinted = false;
|
bool warningPrinted = false;
|
||||||
@override
|
@override
|
||||||
HttpClient createHttpClient(SecurityContext? _) {
|
HttpClient createHttpClient(SecurityContext? context) {
|
||||||
if (!warningPrinted) {
|
if (!warningPrinted) {
|
||||||
test_package.printOnFailure(
|
test_package.printOnFailure(
|
||||||
'Warning: At least one test in this suite creates an HttpClient. When\n'
|
'Warning: At least one test in this suite creates an HttpClient. When '
|
||||||
'running a test suite that uses TestWidgetsFlutterBinding, all HTTP\n'
|
'running a test suite that uses TestWidgetsFlutterBinding, all HTTP '
|
||||||
'requests will return status code 400, and no network request will\n'
|
'requests will return status code 400, and no network request will '
|
||||||
'actually be made. Any test expecting a real network connection and\n'
|
'actually be made. Any test expecting a real network connection and '
|
||||||
'status code will fail.\n'
|
'status code will fail.\n'
|
||||||
'To test code that needs an HttpClient, provide your own HttpClient\n'
|
'To test code that needs an HttpClient, provide your own HttpClient '
|
||||||
'implementation to the code under test, so that your test can\n'
|
'implementation to the code under test, so that your test can '
|
||||||
'consistently provide a testable response to the code under test.');
|
'consistently provide a testable response to the code under test.'
|
||||||
|
.split('\n')
|
||||||
|
.expand<String>((String line) => debugWordWrap(line, FlutterError.wrapWidth))
|
||||||
|
.join('\n'),
|
||||||
|
);
|
||||||
warningPrinted = true;
|
warningPrinted = true;
|
||||||
}
|
}
|
||||||
return _MockHttpClient();
|
return _MockHttpClient();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user