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 {
await _runFlutterTest(path.join(flutterRoot, 'packages', 'fuchsia_remote_debug_protocol'));
await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'non_nullable'));
const String httpClientWarning =
'Warning: At least one test in this suite creates an HttpClient. When\n'
'running a test suite that uses TestWidgetsFlutterBinding, all HTTP\n'
'requests will return status code 400, and no network request will\n'
'actually be made. Any test expecting a real network connection and\n'
'status code will fail.\n'
'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.';
'Warning: At least one test in this suite creates an HttpClient. When running a test suite that uses\n'
'TestWidgetsFlutterBinding, all HTTP requests will return status code 400, and no network request\n'
'will actually be made. Any test expecting a real network connection and status code will fail.\n'
'To test code that needs an HttpClient, provide your own HttpClient implementation to the code under\n'
'test, so that your test can consistently provide a testable response to the code under test.';
await _runFlutterTest(
path.join(flutterRoot, 'packages', 'flutter_test'),
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 });
/// See also:
///
/// * [DebugPrintCallback], for function parameters and usage details.
/// * [debugPrintThrottled], the default implementation.
DebugPrintCallback debugPrint = debugPrintThrottled;
/// 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
/// 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 }) {
final List<String> messageLines = message?.split('\n') ?? <String>['null'];
if (wrapWidth != null) {
......@@ -100,6 +103,9 @@ enum _WordWrapParseMode { inSpace, inWord, atBreak }
/// 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
/// octothorpe ("#", U+0023) are not wrapped (so for example, Dart stack traces
/// won't be wrapped).
......
......@@ -344,6 +344,16 @@ typedef ImageDecoderCallback = Future<ui.Codec> Function(
/// }
/// ```
/// {@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
abstract class ImageProvider<T extends Object> {
/// Abstract const constructor. This constructor enables subclasses to provide
......@@ -596,7 +606,7 @@ abstract class ImageProvider<T extends Object> {
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.
///
/// 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
/// '==' to each other (possibly by using a class for the key that itself
/// 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);
/// 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
/// image.
///
/// For backwards-compatibility the default implementation of this method returns
/// 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.
/// This method is deprecated. Implement [loadImage] instead.
///
/// The [decode] callback provides the logic to obtain the codec for the
/// image.
......@@ -1477,6 +1488,8 @@ class ResizeImage extends ImageProvider<ResizeImageKey> {
/// See also:
///
/// * [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
// 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.
......@@ -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.
///
/// 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;
@override
......
......@@ -309,6 +309,16 @@ typedef ImageErrorWidgetBuilder = Widget Function(
/// using the HTML renderer, the web engine delegates image decoding of network
/// 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:
///
/// * [Icon], which shows an image from a font.
......@@ -819,7 +829,7 @@ class Image extends StatefulWidget {
/// {@end-tool}
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
/// aspect ratio.
......@@ -831,7 +841,7 @@ class Image extends StatefulWidget {
/// and height if the exact image dimensions are not known in advance.
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
/// aspect ratio.
......
......@@ -71,17 +71,21 @@ void mockFlutterAssets() {
class _MockHttpOverrides extends HttpOverrides {
bool warningPrinted = false;
@override
HttpClient createHttpClient(SecurityContext? _) {
HttpClient createHttpClient(SecurityContext? context) {
if (!warningPrinted) {
test_package.printOnFailure(
'Warning: At least one test in this suite creates an HttpClient. When\n'
'running a test suite that uses TestWidgetsFlutterBinding, all HTTP\n'
'requests will return status code 400, and no network request will\n'
'actually be made. Any test expecting a real network connection and\n'
'Warning: At least one test in this suite creates an HttpClient. When '
'running a test suite that uses TestWidgetsFlutterBinding, all HTTP '
'requests will return status code 400, and no network request will '
'actually be made. Any test expecting a real network connection and '
'status code will fail.\n'
'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.');
'To test code that needs an HttpClient, provide your own HttpClient '
'implementation to the code under test, so that your test can '
'consistently provide a testable response to the code under test.'
.split('\n')
.expand<String>((String line) => debugWordWrap(line, FlutterError.wrapWidth))
.join('\n'),
);
warningPrinted = true;
}
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