diff --git a/dev/bots/test.dart b/dev/bots/test.dart index be3405c79e9f9..26fa9c6cdbc55 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -1006,14 +1006,11 @@ Future _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'), diff --git a/examples/api/lib/painting/image_provider/image_provider.0.dart b/examples/api/lib/painting/image_provider/image_provider.0.dart new file mode 100644 index 0000000000000..86349f15c0d0f --- /dev/null +++ b/examples/api/lib/painting/image_provider/image_provider.0.dart @@ -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 { + const CustomNetworkImage(this.url); + + final String url; + + @override + Future obtainKey(ImageConfiguration configuration) { + final Uri result = Uri.parse(url).replace( + queryParameters: { + '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(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 chunkEvents = StreamController(); + debugPrint('Fetching "$key"...'); + return MultiFrameImageStreamCompleter( + codec: _httpClient.getUrl(key) + .then((HttpClientRequest request) => request.close()) + .then((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.error(e, stack); + }) + .whenComplete(chunkEvents.close) + .then(ui.ImmutableBuffer.fromUint8List) + .then(decode), + chunkEvents: chunkEvents.stream, + scale: 1.0, + debugLabel: '"key"', + informationCollector: () => [ + DiagnosticsProperty('Image provider', this), + DiagnosticsProperty('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, + ); + }, + ), + ); + } +} diff --git a/examples/api/test/painting/image_provider/image_provider.0_test.dart b/examples/api/test/painting/image_provider/image_provider.0_test.dart new file mode 100644 index 0000000000000..0b1ce43c0aee6 --- /dev/null +++ b/examples/api/test/painting/image_provider/image_provider.0_test.dart @@ -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 log = []; + 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, ['Fetching "$expectedUrl"...']); + debugPrint = originalDebugPrint; + }); +} diff --git a/packages/flutter/lib/src/foundation/print.dart b/packages/flutter/lib/src/foundation/print.dart index 7f60da7bb6cb9..123e591649a66 100644 --- a/packages/flutter/lib/src/foundation/print.dart +++ b/packages/flutter/lib/src/foundation/print.dart @@ -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 messageLines = message?.split('\n') ?? ['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). diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart index 728ccbff0e44d..0689ad192d423 100644 --- a/packages/flutter/lib/src/painting/image_provider.dart +++ b/packages/flutter/lib/src/painting/image_provider.dart @@ -344,6 +344,16 @@ typedef ImageDecoderCallback = Future 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 { /// Abstract const constructor. This constructor enables subclasses to provide @@ -596,7 +606,7 @@ abstract class ImageProvider { 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 { /// 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 obtainKey(ImageConfiguration configuration); /// Converts a key into an [ImageStreamCompleter], and begins fetching the @@ -632,10 +646,7 @@ abstract class ImageProvider { /// 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 { /// 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 { /// 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? get headers; @override diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart index 4c1ec7321aed6..1e0776cd78047 100644 --- a/packages/flutter/lib/src/widgets/image.dart +++ b/packages/flutter/lib/src/widgets/image.dart @@ -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. diff --git a/packages/flutter_test/lib/src/_binding_io.dart b/packages/flutter_test/lib/src/_binding_io.dart index 710823682ffae..2123956cef468 100644 --- a/packages/flutter_test/lib/src/_binding_io.dart +++ b/packages/flutter_test/lib/src/_binding_io.dart @@ -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 line) => debugWordWrap(line, FlutterError.wrapWidth)) + .join('\n'), + ); warningPrinted = true; } return _MockHttpClient();