[image_picker] getMedia platform changes (#4174)
Adds `getMedia` and `getMultipleMedia` methods to image_picker_platform_interface.
precursor to https://github.com/flutter/packages/pull/3892
part of https://github.com/flutter/flutter/issues/89159
diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
index f93b6ec..bcba89b 100644
--- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
+++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.8.0
+
+* Adds `getMedia` method.
+
## 2.7.0
* Adds `CameraDelegatingImagePickerPlatform` as a base class for platform
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart
index c2c39f9..b21fd29 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart
@@ -253,6 +253,30 @@
}
@override
+ Future<List<XFile>> getMedia({
+ required MediaOptions options,
+ }) async {
+ final ImageOptions imageOptions = options.imageOptions;
+
+ final Map<String, dynamic> args = <String, dynamic>{
+ 'maxImageWidth': imageOptions.maxWidth,
+ 'maxImageHeight': imageOptions.maxHeight,
+ 'imageQuality': imageOptions.imageQuality,
+ 'allowMultiple': options.allowMultiple,
+ };
+
+ final List<XFile>? paths = await _channel
+ .invokeMethod<List<dynamic>?>(
+ 'pickMedia',
+ args,
+ )
+ .then((List<dynamic>? paths) =>
+ paths?.map((dynamic path) => XFile(path as String)).toList());
+
+ return paths ?? <XFile>[];
+ }
+
+ @override
Future<XFile?> getVideo({
required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
@@ -280,13 +304,21 @@
assert(result.containsKey('path') != result.containsKey('errorCode'));
final String? type = result['type'] as String?;
- assert(type == kTypeImage || type == kTypeVideo);
+ assert(
+ type == kTypeImage || type == kTypeVideo || type == kTypeMedia,
+ );
RetrieveType? retrieveType;
- if (type == kTypeImage) {
- retrieveType = RetrieveType.image;
- } else if (type == kTypeVideo) {
- retrieveType = RetrieveType.video;
+ switch (type) {
+ case kTypeImage:
+ retrieveType = RetrieveType.image;
+ break;
+ case kTypeVideo:
+ retrieveType = RetrieveType.video;
+ break;
+ case kTypeMedia:
+ retrieveType = RetrieveType.media;
+ break;
}
PlatformException? exception;
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart
index e01caca..66c5d3b 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart
@@ -213,6 +213,24 @@
throw UnimplementedError('getMultiImage() has not been implemented.');
}
+ /// Returns a [List<XFile>] with the images and/or videos that were picked.
+ /// The images and videos come from the gallery.
+ ///
+ /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and
+ /// above only support HEIC images if used in addition to a size modification,
+ /// of which the usage is explained below.
+ ///
+ /// In Android, the MainActivity can be destroyed for various reasons.
+ /// If that happens, the result will be lost in this call. You can then
+ /// call [getLostData] when your app relaunches to retrieve the lost data.
+ ///
+ /// If no images or videos were picked, the return value is an empty list.
+ Future<List<XFile>> getMedia({
+ required MediaOptions options,
+ }) {
+ throw UnimplementedError('getMedia() has not been implemented.');
+ }
+
/// Returns a [XFile] containing the video that was picked.
///
/// The [source] argument controls where the video comes from. This can
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart
index 2cc01c9..374ff27 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart
@@ -2,6 +2,40 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'types.dart';
+
+/// Specifies options for picking a single image from the device's camera or gallery.
+///
+/// This class inheritance is a byproduct of the api changing over time.
+/// It exists solely to avoid breaking changes.
+class ImagePickerOptions extends ImageOptions {
+ /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality],
+ /// [referredCameraDevice] and [requestFullMetadata].
+ const ImagePickerOptions({
+ super.maxHeight,
+ super.maxWidth,
+ super.imageQuality,
+ super.requestFullMetadata,
+ this.preferredCameraDevice = CameraDevice.rear,
+ }) : super();
+
+ /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality],
+ /// [referredCameraDevice] and [requestFullMetadata].
+ ImagePickerOptions.createAndValidate({
+ super.maxHeight,
+ super.maxWidth,
+ super.imageQuality,
+ super.requestFullMetadata,
+ this.preferredCameraDevice = CameraDevice.rear,
+ }) : super.createAndValidate();
+
+ /// Used to specify the camera to use when the `source` is [ImageSource.camera].
+ ///
+ /// Ignored if the source is not [ImageSource.camera], or the chosen camera is not
+ /// supported on the device. Defaults to [CameraDevice.rear].
+ final CameraDevice preferredCameraDevice;
+}
+
/// Specifies image-specific options for picking.
class ImageOptions {
/// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality]
@@ -13,6 +47,18 @@
this.requestFullMetadata = true,
});
+ /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality]
+ /// and [requestFullMetadata]. Throws if options are not valid.
+ ImageOptions.createAndValidate({
+ this.maxHeight,
+ this.maxWidth,
+ this.imageQuality,
+ this.requestFullMetadata = true,
+ }) {
+ _validateOptions(
+ maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality);
+ }
+
/// The maximum width of the image, in pixels.
///
/// If null, the image will only be resized if [maxHeight] is specified.
@@ -38,4 +84,19 @@
//
// Defaults to true.
final bool requestFullMetadata;
+
+ /// Validates that all values are within required ranges. Throws if not.
+ static void _validateOptions(
+ {double? maxWidth, final double? maxHeight, int? imageQuality}) {
+ if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
+ throw ArgumentError.value(
+ imageQuality, 'imageQuality', 'must be between 0 and 100');
+ }
+ if (maxWidth != null && maxWidth < 0) {
+ throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative');
+ }
+ if (maxHeight != null && maxHeight < 0) {
+ throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
+ }
+ }
}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart
deleted file mode 100644
index 0d85c91..0000000
--- a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2013 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 'types.dart';
-
-/// Specifies options for picking a single image from the device's camera or gallery.
-class ImagePickerOptions {
- /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality],
- /// [referredCameraDevice] and [requestFullMetadata].
- const ImagePickerOptions({
- this.maxHeight,
- this.maxWidth,
- this.imageQuality,
- this.preferredCameraDevice = CameraDevice.rear,
- this.requestFullMetadata = true,
- });
-
- /// The maximum width of the image, in pixels.
- ///
- /// If null, the image will only be resized if [maxHeight] is specified.
- final double? maxWidth;
-
- /// The maximum height of the image, in pixels.
- ///
- /// If null, the image will only be resized if [maxWidth] is specified.
- final double? maxHeight;
-
- /// Modifies the quality of the image, ranging from 0-100 where 100 is the
- /// original/max quality.
- ///
- /// Compression is only supported for certain image types such as JPEG. If
- /// compression is not supported for the image that is picked, a warning
- /// message will be logged.
- ///
- /// If null, the image will be returned with the original quality.
- final int? imageQuality;
-
- /// Used to specify the camera to use when the `source` is [ImageSource.camera].
- ///
- /// Ignored if the source is not [ImageSource.camera], or the chosen camera is not
- /// supported on the device. Defaults to [CameraDevice.rear].
- final CameraDevice preferredCameraDevice;
-
- /// If true, requests full image metadata, which may require extra permissions
- /// on some platforms, (e.g., NSPhotoLibraryUsageDescription on iOS).
- //
- // Defaults to true.
- final bool requestFullMetadata;
-}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart
index 10af812..0f802f1 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart
@@ -36,7 +36,8 @@
/// An empty response should have [file], [exception] and [type] to be null.
bool get isEmpty => _empty;
- /// The file that was lost in a previous [getImage], [getMultiImage] or [getVideo] call due to MainActivity being destroyed.
+ /// The file that was lost in a previous [getImage], [getMultiImage],
+ /// [getVideo] or [getMedia] call due to MainActivity being destroyed.
///
/// Can be null if [exception] exists.
final XFile? file;
@@ -51,7 +52,7 @@
/// Note that it is not the exception that caused the destruction of the MainActivity.
final PlatformException? exception;
- /// Can either be [RetrieveType.image] or [RetrieveType.video];
+ /// Can either be [RetrieveType.image], [RetrieveType.video], or [RetrieveType.media].
///
/// If the lost data is empty, this will be null.
final RetrieveType? type;
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart
new file mode 100644
index 0000000..70a048f
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart
@@ -0,0 +1,23 @@
+// Copyright 2013 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 '../../image_picker_platform_interface.dart';
+
+/// Specifies options for selecting items when using [ImagePickerPlatform.getMedia].
+@immutable
+class MediaOptions {
+ /// Construct a new MediaOptions instance.
+ const MediaOptions({
+ this.imageOptions = const ImageOptions(),
+ required this.allowMultiple,
+ });
+
+ /// Options that will apply to images upon selection.
+ final ImageOptions imageOptions;
+
+ /// Whether to allow for selecting multiple media.
+ final bool allowMultiple;
+}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart
new file mode 100644
index 0000000..cd01134
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart
@@ -0,0 +1,14 @@
+// Copyright 2013 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 '../../image_picker_platform_interface.dart';
+
+/// The type of media to allow the user to select with [ImagePickerPlatform.getMedia].
+enum MediaSelectionType {
+ /// Static pictures.
+ image,
+
+ /// Videos.
+ video,
+}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart
index 445445e..94fed59 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart
@@ -8,5 +8,8 @@
image,
/// A video. See [ImagePicker.pickVideo].
- video
+ video,
+
+ /// Either a video or a static picture. See [ImagePicker.pickMedia].
+ media,
}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart
index fcb76cc..0339d98 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart
@@ -5,9 +5,10 @@
export 'camera_delegate.dart';
export 'camera_device.dart';
export 'image_options.dart';
-export 'image_picker_options.dart';
export 'image_source.dart';
export 'lost_data_response.dart';
+export 'media_options.dart';
+export 'media_selection_type.dart';
export 'multi_image_picker_options.dart';
export 'picked_file/picked_file.dart';
export 'retrieve_type.dart';
@@ -17,3 +18,6 @@
/// Denotes that a video is being picked.
const String kTypeVideo = 'video';
+
+/// Denotes that either a video or image is being picked.
+const String kTypeMedia = 'media';
diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml
index 3f1e523..67a5070 100644
--- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml
+++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml
@@ -4,7 +4,7 @@
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
-version: 2.7.0
+version: 2.8.0
environment:
sdk: ">=2.18.0 <4.0.0"
diff --git a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart
index 244af39..cf92c2c 100644
--- a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart
+++ b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart
@@ -872,6 +872,152 @@
});
});
+ group('#getMedia', () {
+ test('calls the method correctly', () async {
+ returnValue = <String>['0'];
+ await picker.getMedia(options: const MediaOptions(allowMultiple: true));
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('pickMedia', arguments: <String, dynamic>{
+ 'maxImageWidth': null,
+ 'maxImageHeight': null,
+ 'imageQuality': null,
+ 'allowMultiple': true,
+ }),
+ ],
+ );
+ });
+
+ test('passes the selection options correctly', () async {
+ // Default options
+ returnValue = <String>['0'];
+ await picker.getMedia(options: const MediaOptions(allowMultiple: true));
+ // Various image options
+ returnValue = <String>['0'];
+ await picker.getMedia(
+ options: MediaOptions(
+ allowMultiple: true,
+ imageOptions: ImageOptions.createAndValidate(
+ maxWidth: 10.0,
+ ),
+ ),
+ );
+ await picker.getMedia(
+ options: MediaOptions(
+ allowMultiple: true,
+ imageOptions: ImageOptions.createAndValidate(
+ maxHeight: 10.0,
+ ),
+ ),
+ );
+ await picker.getMedia(
+ options: MediaOptions(
+ allowMultiple: true,
+ imageOptions: ImageOptions.createAndValidate(
+ imageQuality: 70,
+ ),
+ ),
+ );
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('pickMedia', arguments: <String, dynamic>{
+ 'maxImageWidth': null,
+ 'maxImageHeight': null,
+ 'imageQuality': null,
+ 'allowMultiple': true,
+ }),
+ isMethodCall('pickMedia', arguments: <String, dynamic>{
+ 'maxImageWidth': 10.0,
+ 'maxImageHeight': null,
+ 'imageQuality': null,
+ 'allowMultiple': true,
+ }),
+ isMethodCall('pickMedia', arguments: <String, dynamic>{
+ 'maxImageWidth': null,
+ 'maxImageHeight': 10.0,
+ 'imageQuality': null,
+ 'allowMultiple': true,
+ }),
+ isMethodCall('pickMedia', arguments: <String, dynamic>{
+ 'maxImageWidth': null,
+ 'maxImageHeight': null,
+ 'imageQuality': 70,
+ 'allowMultiple': true,
+ }),
+ ],
+ );
+ });
+
+ test('does not accept a negative width or height argument', () {
+ returnValue = <String>['0', '1'];
+ expect(
+ () => picker.getMedia(
+ options: MediaOptions(
+ allowMultiple: true,
+ imageOptions: ImageOptions.createAndValidate(
+ maxWidth: -1.0,
+ ),
+ ),
+ ),
+ throwsArgumentError,
+ );
+
+ expect(
+ () => picker.getMedia(
+ options: MediaOptions(
+ allowMultiple: true,
+ imageOptions: ImageOptions.createAndValidate(
+ maxHeight: -1.0,
+ ),
+ ),
+ ),
+ throwsArgumentError,
+ );
+ });
+
+ test('does not accept a invalid imageQuality argument', () {
+ returnValue = <String>['0', '1'];
+ expect(
+ () => picker.getMedia(
+ options: MediaOptions(
+ allowMultiple: true,
+ imageOptions: ImageOptions.createAndValidate(
+ imageQuality: -1,
+ ),
+ ),
+ ),
+ throwsArgumentError,
+ );
+
+ expect(
+ () => picker.getMedia(
+ options: MediaOptions(
+ allowMultiple: true,
+ imageOptions: ImageOptions.createAndValidate(
+ imageQuality: 101,
+ ),
+ ),
+ ),
+ throwsArgumentError,
+ );
+ });
+
+ test('handles a null path response gracefully', () async {
+ _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
+ .defaultBinaryMessenger
+ .setMockMethodCallHandler(
+ picker.channel, (MethodCall methodCall) => null);
+ expect(
+ await picker.getMedia(
+ options: const MediaOptions(allowMultiple: true)),
+ <XFile>[]);
+ });
+ });
+
group('#getVideo', () {
test('passes the image source argument correctly', () async {
await picker.getVideo(source: ImageSource.camera);