Unverified Commit 9c8f3950 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

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
parent 702b78c6
...@@ -1006,14 +1006,11 @@ Future<void> _runFrameworkTests() async { ...@@ -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'),
......
// 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,
);
},
),
);
}
}
// 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 }); ...@@ -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 }) { ...@@ -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 } ...@@ -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( ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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( ...@@ -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 { ...@@ -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 { ...@@ -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() { ...@@ -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();
......
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