From 3a21cfac861aa42f3274b1b27d78a7a4bb9fbcca Mon Sep 17 00:00:00 2001 From: Piotr Denert Date: Sun, 7 Apr 2024 22:51:20 +0200 Subject: [PATCH] Add limit to image_picker_platform_interface (#6434) Adds limit parameter to `MediaOptions` and `MultiImagePickerOptions`. The `limit` argument defines how many images or media files can be select. Only platform interface package changes taken from: https://github.com/flutter/packages/pull/6201 Fixes: [flutter/flutter#85772](https://github.com/flutter/flutter/issues/85772) --- .../CHANGELOG.md | 4 + .../method_channel_image_picker.dart | 4 + .../lib/src/types/media_options.dart | 37 +++++ .../src/types/multi_image_picker_options.dart | 28 +++- .../pubspec.yaml | 2 +- .../test/media_options_test.dart | 54 +++++++ .../method_channel_image_picker_test.dart | 147 ++++++++++++++++-- .../test/multi_image_picker_options_test.dart | 32 ++++ 8 files changed, 296 insertions(+), 12 deletions(-) create mode 100644 packages/image_picker/image_picker_platform_interface/test/media_options_test.dart create mode 100644 packages/image_picker/image_picker_platform_interface/test/multi_image_picker_options_test.dart diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index ef63d598bb83..28a9d4b2e1a8 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.10.0 + +* Adds limit parameter to `MediaOptions` and `MultiImagePickerOptions`. + ## 2.9.4 * Updates minimum supported SDK version to Flutter 3.19/Dart 3.3. 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 95f571c6df7e..943ea760995b 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 @@ -58,6 +58,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxHeight, int? imageQuality, bool requestFullMetadata = true, + int? limit, }) { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( @@ -79,6 +80,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { 'maxHeight': maxHeight, 'imageQuality': imageQuality, 'requestFullMetadata': requestFullMetadata, + 'limit': limit, }, ); } @@ -244,6 +246,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { maxHeight: options.imageOptions.maxHeight, imageQuality: options.imageOptions.imageQuality, requestFullMetadata: options.imageOptions.requestFullMetadata, + limit: options.limit, ); if (paths == null) { return []; @@ -263,6 +266,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { 'maxImageHeight': imageOptions.maxHeight, 'imageQuality': imageOptions.imageQuality, 'allowMultiple': options.allowMultiple, + 'limit': options.limit, }; final List? paths = await _channel 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 index 70a048f7147d..9576cadbad8a 100644 --- 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 @@ -13,11 +13,48 @@ class MediaOptions { const MediaOptions({ this.imageOptions = const ImageOptions(), required this.allowMultiple, + this.limit, }); + /// Construct a new MediaOptions instance. + /// + /// Throws if limit is lower than 2, + /// or allowMultiple is false and limit is not null. + MediaOptions.createAndValidate({ + this.imageOptions = const ImageOptions(), + required this.allowMultiple, + this.limit, + }) { + _validate(allowMultiple: allowMultiple, limit: limit); + } + /// Options that will apply to images upon selection. final ImageOptions imageOptions; /// Whether to allow for selecting multiple media. final bool allowMultiple; + + /// The maximum number of images to select. + /// + /// Default null value means no limit. + /// This value may be ignored by platforms that cannot support it. + final int? limit; + + /// Validates that all values are within required ranges. + /// + /// Throws if limit is lower than 2, + /// or allowMultiple is false and limit is not null. + static void _validate({required bool allowMultiple, int? limit}) { + if (!allowMultiple && limit != null) { + throw ArgumentError.value( + allowMultiple, + 'allowMultiple', + 'cannot be false, when limit is not null', + ); + } + + if (limit != null && limit < 2) { + throw ArgumentError.value(limit, 'limit', 'cannot be lower then 2'); + } + } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/multi_image_picker_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/multi_image_picker_options.dart index c860297ce33f..500f4938b9bd 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/multi_image_picker_options.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/multi_image_picker_options.dart @@ -6,11 +6,37 @@ import 'image_options.dart'; /// Specifies options for picking multiple images from the device's gallery. class MultiImagePickerOptions { - /// Creates an instance with the given [imageOptions]. + /// Creates an instance with the given [imageOptions] and [limit]. const MultiImagePickerOptions({ this.imageOptions = const ImageOptions(), + this.limit, }); + /// Creates an instance with the given [imageOptions] and [limit]. + /// + /// Throws if limit is lower than 2. + MultiImagePickerOptions.createAndValidate({ + this.imageOptions = const ImageOptions(), + this.limit, + }) { + _validate(limit: limit); + } + /// The image-specific options for picking. final ImageOptions imageOptions; + + /// The maximum number of images to select. + /// + /// Default null value means no limit. + /// This value may be ignored by platforms that cannot support it. + final int? limit; + + /// Validates that all values are within required ranges. + /// + /// Throws if limit is lower than 2. + static void _validate({int? limit}) { + if (limit != null && limit < 2) { + throw ArgumentError.value(limit, 'limit', 'cannot be lower then 2'); + } + } } diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index b38945c6dac7..9af992c7c7bf 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 @@ repository: https://github.com/flutter/packages/tree/main/packages/image_picker/ 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.9.4 +version: 2.10.0 environment: sdk: ^3.3.0 diff --git a/packages/image_picker/image_picker_platform_interface/test/media_options_test.dart b/packages/image_picker/image_picker_platform_interface/test/media_options_test.dart new file mode 100644 index 000000000000..44a5f58c8cbf --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/media_options_test.dart @@ -0,0 +1,54 @@ +// 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_test/flutter_test.dart'; +import 'package:image_picker_platform_interface/src/types/types.dart'; + +void main() { + group('MediaOptions', () { + test('createAndValidate does not throw when allowMultiple is true', () { + expect( + () => MediaOptions.createAndValidate(allowMultiple: true), + returnsNormally, + ); + }); + test('createAndValidate does not throw when allowMultiple is false', () { + expect( + () => MediaOptions.createAndValidate(allowMultiple: false), + returnsNormally, + ); + }); + + test('createAndValidate does not throw error for correct limit', () { + expect( + () => MediaOptions.createAndValidate(allowMultiple: true, limit: 2), + returnsNormally, + ); + }); + + test('createAndValidate throw error for to small limit', () { + expect( + () => MediaOptions.createAndValidate(allowMultiple: true, limit: 1), + throwsArgumentError, + ); + expect( + () => MediaOptions.createAndValidate(allowMultiple: true, limit: 0), + throwsArgumentError, + ); + expect( + () => MediaOptions.createAndValidate(allowMultiple: true, limit: -1), + throwsArgumentError, + ); + }); + + test( + 'createAndValidate throw error when allowMultiple is false and has limit', + () { + expect( + () => MediaOptions.createAndValidate(allowMultiple: false, limit: 2), + throwsArgumentError, + ); + }); + }); +} 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 a5facb4dda9e..abc696711014 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 @@ -247,6 +247,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -287,42 +288,49 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -745,6 +753,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -785,42 +794,49 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -865,7 +881,8 @@ void main() { group('#getMedia', () { test('calls the method correctly', () async { returnValue = ['0']; - await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + await picker.getMedia( + options: MediaOptions.createAndValidate(allowMultiple: true)); expect( log, @@ -875,6 +892,7 @@ void main() { 'maxImageHeight': null, 'imageQuality': null, 'allowMultiple': true, + 'limit': null, }), ], ); @@ -883,11 +901,12 @@ void main() { test('passes the selection options correctly', () async { // Default options returnValue = ['0']; - await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + await picker.getMedia( + options: MediaOptions.createAndValidate(allowMultiple: true)); // Various image options returnValue = ['0']; await picker.getMedia( - options: MediaOptions( + options: MediaOptions.createAndValidate( allowMultiple: true, imageOptions: ImageOptions.createAndValidate( maxWidth: 10.0, @@ -895,7 +914,7 @@ void main() { ), ); await picker.getMedia( - options: MediaOptions( + options: MediaOptions.createAndValidate( allowMultiple: true, imageOptions: ImageOptions.createAndValidate( maxHeight: 10.0, @@ -903,11 +922,20 @@ void main() { ), ); await picker.getMedia( - options: MediaOptions( + options: MediaOptions.createAndValidate( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + imageQuality: 70, + ), + ), + ); + await picker.getMedia( + options: MediaOptions.createAndValidate( allowMultiple: true, imageOptions: ImageOptions.createAndValidate( imageQuality: 70, ), + limit: 5, ), ); @@ -919,24 +947,35 @@ void main() { 'maxImageHeight': null, 'imageQuality': null, 'allowMultiple': true, + 'limit': null, }), isMethodCall('pickMedia', arguments: { 'maxImageWidth': 10.0, 'maxImageHeight': null, 'imageQuality': null, 'allowMultiple': true, + 'limit': null, }), isMethodCall('pickMedia', arguments: { 'maxImageWidth': null, 'maxImageHeight': 10.0, 'imageQuality': null, 'allowMultiple': true, + 'limit': null, }), isMethodCall('pickMedia', arguments: { 'maxImageWidth': null, 'maxImageHeight': null, 'imageQuality': 70, 'allowMultiple': true, + 'limit': null, + }), + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': null, + 'imageQuality': 70, + 'allowMultiple': true, + 'limit': 5, }), ], ); @@ -946,7 +985,7 @@ void main() { returnValue = ['0', '1']; expect( () => picker.getMedia( - options: MediaOptions( + options: MediaOptions.createAndValidate( allowMultiple: true, imageOptions: ImageOptions.createAndValidate( maxWidth: -1.0, @@ -958,7 +997,7 @@ void main() { expect( () => picker.getMedia( - options: MediaOptions( + options: MediaOptions.createAndValidate( allowMultiple: true, imageOptions: ImageOptions.createAndValidate( maxHeight: -1.0, @@ -973,7 +1012,7 @@ void main() { returnValue = ['0', '1']; expect( () => picker.getMedia( - options: MediaOptions( + options: MediaOptions.createAndValidate( allowMultiple: true, imageOptions: ImageOptions.createAndValidate( imageQuality: -1, @@ -985,7 +1024,7 @@ void main() { expect( () => picker.getMedia( - options: MediaOptions( + options: MediaOptions.createAndValidate( allowMultiple: true, imageOptions: ImageOptions.createAndValidate( imageQuality: 101, @@ -996,13 +1035,44 @@ void main() { ); }); + test('does not accept a invalid limit argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: + MediaOptions.createAndValidate(allowMultiple: true, limit: -1), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions.createAndValidate( + allowMultiple: true, + limit: 0, + ), + ), + throwsArgumentError, + ); + }); + + test('does not accept a not null limit when allowMultiple is false', () { + expect( + () => picker.getMedia( + options: + MediaOptions.createAndValidate(allowMultiple: false, limit: 5), + ), + throwsArgumentError, + ); + }); + test('handles a null path response gracefully', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( picker.channel, (MethodCall methodCall) => null); expect( await picker.getMedia( - options: const MediaOptions(allowMultiple: true)), + options: MediaOptions.createAndValidate(allowMultiple: true)), []); }); }); @@ -1458,6 +1528,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -1510,6 +1581,16 @@ void main() { ), ), ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + limit: 5, + ), + ); expect( log, @@ -1519,42 +1600,56 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'limit': 5, }), ], ); @@ -1602,6 +1697,36 @@ void main() { ); }); + test('does not accept an invalid limit argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMultiImageWithOptions( + options: MultiImagePickerOptions.createAndValidate( + limit: -1, + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImageWithOptions( + options: MultiImagePickerOptions.createAndValidate( + limit: 0, + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImageWithOptions( + options: MultiImagePickerOptions.createAndValidate( + limit: 1, + ), + ), + throwsArgumentError, + ); + }); + test('handles a null image path response gracefully', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( @@ -1623,6 +1748,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -1644,6 +1770,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': false, + 'limit': null, }), ], ); diff --git a/packages/image_picker/image_picker_platform_interface/test/multi_image_picker_options_test.dart b/packages/image_picker/image_picker_platform_interface/test/multi_image_picker_options_test.dart new file mode 100644 index 000000000000..9a4b876fce14 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/multi_image_picker_options_test.dart @@ -0,0 +1,32 @@ +// 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_test/flutter_test.dart'; +import 'package:image_picker_platform_interface/src/types/types.dart'; + +void main() { + group('MultiImagePickerOptions', () { + test('createAndValidate does not throw error for correct limit', () { + expect( + () => MultiImagePickerOptions.createAndValidate(limit: 2), + returnsNormally, + ); + }); + + test('createAndValidate throw error for to small limit', () { + expect( + () => MultiImagePickerOptions.createAndValidate(limit: 1), + throwsArgumentError, + ); + expect( + () => MultiImagePickerOptions.createAndValidate(limit: 0), + throwsArgumentError, + ); + expect( + () => MultiImagePickerOptions.createAndValidate(limit: -1), + throwsArgumentError, + ); + }); + }); +}