[image_picker_web] Listens to file input cancel event. (#4453)
## Changes
This PR listens to the `cancel` event from the `input type=file` used by the web implementation of the image_picker plugin, so apps don't end up endlessly awaiting for a file that will never come **in modern browsers** (Chrome 113, Safari 16.4, or newer). _Same API as https://github.com/flutter/packages/pull/3683._
Additionally, this PR:
* Removes all code and tests mentioning `PickedFile`. (Deprecated years ago, and unused since https://github.com/flutter/packages/pull/4285) **(Breaking change)**
* Updates README to mention `XFile` which is the current return type of the package.
* Updates the dependency on `image_picker_platform_interface` to `^2.9.0`.
* Implements all non-deprecated methods from the interface, and makes deprecated methods use the fresh ones.
* Updates tests.
### Issues
* Fixes https://github.com/flutter/flutter/issues/92176
### Testing
* Added integration testing coverage for the 'cancel' event.
* Tested manually in Chrome with the example app running on web.
diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md
index 8230dd7..88f6159 100644
--- a/packages/image_picker/image_picker_for_web/CHANGELOG.md
+++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 3.0.0
+
+* **BREAKING CHANGE:** Removes all code and tests mentioning `PickedFile`.
+* Listens to `cancel` event on file selection. When the selection is canceled:
+ * `Future<XFile?>` methods return `null`
+ * `Future<List<XFile>>` methods return an empty list.
+
## 2.2.0
* Adds `getMedia` method.
diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md
index 4d8db43..6583105 100644
--- a/packages/image_picker/image_picker_for_web/README.md
+++ b/packages/image_picker/image_picker_for_web/README.md
@@ -4,23 +4,12 @@
## Limitations on the web platform
-Since Web Browsers don't offer direct access to their users' file system,
-this plugin provides a `PickedFile` abstraction to make access uniform
-across platforms.
+### `XFile`
-The web version of the plugin puts network-accessible URIs as the `path`
-in the returned `PickedFile`.
+This plugin uses `XFile` objects to abstract files picked/created by the user.
-### URL.createObjectURL()
-
-The `PickedFile` object in web is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL),
-which is reasonably well supported across all browsers:
-
-![Data on support for the bloburls feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/bloburls.png)
-
-However, the returned `path` attribute of the `PickedFile` points to a `network` resource, and not a
-local path in your users' drive. See **Use the plugin** below for some examples on how to use this
-return value in a cross-platform way.
+Read more about `XFile` on the web in
+[`package:cross_file`'s README](https://pub.dev/packages/cross_file).
### input file "accept"
@@ -42,11 +31,26 @@
Each browser may implement `capture` any way they please, so it may (or may not) make a
difference in your users' experience.
-### pickImage()
-The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported for gif images.
-The argument `imageQuality` only works for jpeg and webp images.
+### input file "cancel"
-### pickVideo()
+The [`cancel` event](https://caniuse.com/mdn-api_htmlinputelement_cancel_event)
+used by the plugin to detect when users close the file selector without picking
+a file is relatively new, and will only work in recent browsers.
+
+### `ImagePickerOptions` support
+
+The `ImagePickerOptions` configuration object allows passing resize (`maxWidth`,
+`maxHeight`) and quality (`imageQuality`) parameters to some methods of this
+plugin, which in other platforms control how selected images are resized or
+re-encoded.
+
+On the web:
+
+* `maxWidth`, `maxHeight` and `imageQuality` are not supported for `gif` images.
+* `imageQuality` only affects `jpg` and `webp` images.
+
+### `getVideo()`
+
The argument `maxDuration` is not supported on the web.
## Usage
@@ -65,8 +69,8 @@
You should be able to use `package:image_picker` _almost_ as normal.
-Once the user has picked a file, the returned `PickedFile` instance will contain a
-`network`-accessible URL (pointing to a location within the browser).
+Once the user has picked a file, the returned `XFile` instance will contain a
+`network`-accessible `Blob` URL (pointing to a location within the browser).
The instance will also let you retrieve the bytes of the selected file across all platforms.
diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart
index fffbd6d..5a3af7e 100644
--- a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart
+++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart
@@ -33,7 +33,9 @@
plugin = ImagePickerPlugin();
});
- testWidgets('Can select a file (Deprecated)', (WidgetTester tester) async {
+ testWidgets('getImageFromSource can select a file', (
+ WidgetTester _,
+ ) async {
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
final ImagePickerPluginTestOverrides overrides =
@@ -44,29 +46,9 @@
final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);
// Init the pick file dialog...
- final Future<PickedFile> file = plugin.pickFile();
-
- // Mock the browser behavior of selecting a file...
- mockInput.dispatchEvent(html.Event('change'));
-
- // Now the file should be available
- expect(file, completes);
- // And readable
- expect((await file).readAsBytes(), completion(isNotEmpty));
- });
-
- testWidgets('Can select a file', (WidgetTester tester) async {
- final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
-
- final ImagePickerPluginTestOverrides overrides =
- ImagePickerPluginTestOverrides()
- ..createInputElement = ((_, __) => mockInput)
- ..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
-
- final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);
-
- // Init the pick file dialog...
- final Future<XFile> image = plugin.getImage(source: ImageSource.camera);
+ final Future<XFile?> image = plugin.getImageFromSource(
+ source: ImageSource.camera,
+ );
// Mock the browser behavior of selecting a file...
mockInput.dispatchEvent(html.Event('change'));
@@ -75,8 +57,9 @@
expect(image, completes);
// And readable
- final XFile file = await image;
- expect(file.readAsBytes(), completion(isNotEmpty));
+ final XFile? file = await image;
+ expect(file, isNotNull);
+ expect(file!.readAsBytes(), completion(isNotEmpty));
expect(file.name, textFile.name);
expect(file.length(), completion(textFile.size));
expect(file.mimeType, textFile.type);
@@ -87,8 +70,9 @@
));
});
- testWidgets('getMultiImage can select multiple files',
- (WidgetTester tester) async {
+ testWidgets('getMultiImageWithOptions can select multiple files', (
+ WidgetTester _,
+ ) async {
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
final ImagePickerPluginTestOverrides overrides =
@@ -100,7 +84,7 @@
final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);
// Init the pick file dialog...
- final Future<List<XFile>> files = plugin.getMultiImage();
+ final Future<List<XFile>> files = plugin.getMultiImageWithOptions();
// Mock the browser behavior of selecting a file...
mockInput.dispatchEvent(html.Event('change'));
@@ -118,8 +102,7 @@
expect(secondFile.length(), completion(secondTextFile.size));
});
- testWidgets('getMedia can select multiple files',
- (WidgetTester tester) async {
+ testWidgets('getMedia can select multiple files', (WidgetTester _) async {
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
final ImagePickerPluginTestOverrides overrides =
@@ -150,7 +133,72 @@
expect(secondFile.length(), completion(secondTextFile.size));
});
- // There's no good way of detecting when the user has "aborted" the selection.
+ group('cancel event', () {
+ late html.FileUploadInputElement mockInput;
+ late ImagePickerPluginTestOverrides overrides;
+ late ImagePickerPlugin plugin;
+
+ setUp(() {
+ mockInput = html.FileUploadInputElement();
+ overrides = ImagePickerPluginTestOverrides()
+ ..createInputElement = ((_, __) => mockInput)
+ ..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
+ plugin = ImagePickerPlugin(overrides: overrides);
+ });
+
+ void mockCancel() {
+ mockInput.dispatchEvent(html.Event('cancel'));
+ }
+
+ testWidgets('getFiles - returns empty list', (WidgetTester _) async {
+ final Future<List<XFile>> files = plugin.getFiles();
+ mockCancel();
+
+ expect(files, completes);
+ expect(await files, isEmpty);
+ });
+
+ testWidgets('getMedia - returns empty list', (WidgetTester _) async {
+ final Future<List<XFile>?> files = plugin.getMedia(
+ options: const MediaOptions(
+ allowMultiple: true,
+ ));
+ mockCancel();
+
+ expect(files, completes);
+ expect(await files, isEmpty);
+ });
+
+ testWidgets('getMultiImageWithOptions - returns empty list', (
+ WidgetTester _,
+ ) async {
+ final Future<List<XFile>?> files = plugin.getMultiImageWithOptions();
+ mockCancel();
+
+ expect(files, completes);
+ expect(await files, isEmpty);
+ });
+
+ testWidgets('getImageFromSource - returns null', (WidgetTester _) async {
+ final Future<XFile?> file = plugin.getImageFromSource(
+ source: ImageSource.gallery,
+ );
+ mockCancel();
+
+ expect(file, completes);
+ expect(await file, isNull);
+ });
+
+ testWidgets('getVideo - returns null', (WidgetTester _) async {
+ final Future<XFile?> file = plugin.getVideo(
+ source: ImageSource.gallery,
+ );
+ mockCancel();
+
+ expect(file, completes);
+ expect(await file, isNull);
+ });
+ });
testWidgets('computeCaptureAttribute', (WidgetTester tester) async {
expect(
@@ -208,4 +256,102 @@
expect(input.attributes, contains('multiple'));
});
});
+
+ group('Deprecated methods', () {
+ late html.FileUploadInputElement mockInput;
+ late ImagePickerPluginTestOverrides overrides;
+ late ImagePickerPlugin plugin;
+
+ setUp(() {
+ mockInput = html.FileUploadInputElement();
+ overrides = ImagePickerPluginTestOverrides()
+ ..createInputElement = ((_, __) => mockInput)
+ ..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
+ plugin = ImagePickerPlugin(overrides: overrides);
+ });
+
+ void mockCancel() {
+ mockInput.dispatchEvent(html.Event('cancel'));
+ }
+
+ void mockChange() {
+ mockInput.dispatchEvent(html.Event('change'));
+ }
+
+ group('getImage', () {
+ testWidgets('can select a file', (WidgetTester _) async {
+ // ignore: deprecated_member_use
+ final Future<XFile?> image = plugin.getImage(
+ source: ImageSource.camera,
+ );
+
+ // Mock the browser behavior when selecting a file...
+ mockChange();
+
+ // Now the file should be available
+ expect(image, completes);
+
+ // And readable
+ final XFile? file = await image;
+ expect(file, isNotNull);
+ expect(file!.readAsBytes(), completion(isNotEmpty));
+ expect(file.name, textFile.name);
+ expect(file.length(), completion(textFile.size));
+ expect(file.mimeType, textFile.type);
+ expect(
+ file.lastModified(),
+ completion(
+ DateTime.fromMillisecondsSinceEpoch(textFile.lastModified!),
+ ));
+ });
+
+ testWidgets('returns null when canceled', (WidgetTester _) async {
+ // ignore: deprecated_member_use
+ final Future<XFile?> file = plugin.getImage(
+ source: ImageSource.gallery,
+ );
+ mockCancel();
+
+ expect(file, completes);
+ expect(await file, isNull);
+ });
+ });
+
+ group('getMultiImage', () {
+ testWidgets('can select multiple files', (WidgetTester _) async {
+ // Override the returned files...
+ overrides.getMultipleFilesFromInput =
+ (_) => <html.File>[textFile, secondTextFile];
+
+ // ignore: deprecated_member_use
+ final Future<List<XFile>> files = plugin.getMultiImage();
+
+ // Mock the browser behavior of selecting a file...
+ mockChange();
+
+ // Now the file should be available
+ expect(files, completes);
+
+ // And readable
+ expect((await files).first.readAsBytes(), completion(isNotEmpty));
+
+ // Peek into the second file...
+ final XFile secondFile = (await files).elementAt(1);
+ expect(secondFile.readAsBytes(), completion(isNotEmpty));
+ expect(secondFile.name, secondTextFile.name);
+ expect(secondFile.length(), completion(secondTextFile.size));
+ });
+
+ testWidgets('returns an empty list when canceled', (
+ WidgetTester _,
+ ) async {
+ // ignore: deprecated_member_use
+ final Future<List<XFile>?> files = plugin.getMultiImage();
+ mockCancel();
+
+ expect(files, completes);
+ expect(await files, isEmpty);
+ });
+ });
+ });
}
diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
index fb88c96..b54b68a 100644
--- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
+++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
@@ -42,102 +42,47 @@
ImagePickerPlatform.instance = ImagePickerPlugin();
}
- /// Returns a [PickedFile] with the image that was picked.
- ///
- /// The `source` argument controls where the image comes from. This can
- /// be either [ImageSource.camera] or [ImageSource.gallery].
- ///
- /// Note that the `maxWidth`, `maxHeight` and `imageQuality` arguments are not supported on the web. If any of these arguments is supplied, it'll be silently ignored by the web version of the plugin.
- ///
- /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
- /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
- /// Defaults to [CameraDevice.rear].
- ///
- /// If no images were picked, the return value is null.
+ /// Returns an [XFile] with the image that was picked, or `null` if no images were picked.
@override
- Future<PickedFile> pickImage({
+ Future<XFile?> getImageFromSource({
required ImageSource source,
- double? maxWidth,
- double? maxHeight,
- int? imageQuality,
- CameraDevice preferredCameraDevice = CameraDevice.rear,
- }) {
- final String? capture =
- computeCaptureAttribute(source, preferredCameraDevice);
- return pickFile(accept: _kAcceptImageMimeType, capture: capture);
- }
-
- /// Returns a [PickedFile] containing the video that was picked.
- ///
- /// The [source] argument controls where the video comes from. This can
- /// be either [ImageSource.camera] or [ImageSource.gallery].
- ///
- /// Note that the `maxDuration` argument is not supported on the web. If the argument is supplied, it'll be silently ignored by the web version of the plugin.
- ///
- /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
- /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
- /// Defaults to [CameraDevice.rear].
- ///
- /// If no images were picked, the return value is null.
- @override
- Future<PickedFile> pickVideo({
- required ImageSource source,
- CameraDevice preferredCameraDevice = CameraDevice.rear,
- Duration? maxDuration,
- }) {
- final String? capture =
- computeCaptureAttribute(source, preferredCameraDevice);
- return pickFile(accept: _kAcceptVideoMimeType, capture: capture);
- }
-
- /// Injects a file input with the specified accept+capture attributes, and
- /// returns the PickedFile that the user selected locally.
- ///
- /// `capture` is only supported in mobile browsers.
- /// See https://caniuse.com/#feat=html-media-capture
- @visibleForTesting
- Future<PickedFile> pickFile({
- String? accept,
- String? capture,
- }) {
- final html.FileUploadInputElement input =
- createInputElement(accept, capture) as html.FileUploadInputElement;
- _injectAndActivate(input);
- return _getSelectedFile(input);
- }
-
- /// Returns an [XFile] with the image that was picked.
- ///
- /// The `source` argument controls where the image comes from. This can
- /// be either [ImageSource.camera] or [ImageSource.gallery].
- ///
- /// Note that the `maxWidth`, `maxHeight` and `imageQuality` arguments are not supported on the web. If any of these arguments is supplied, it'll be silently ignored by the web version of the plugin.
- ///
- /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
- /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
- /// Defaults to [CameraDevice.rear].
- ///
- /// If no images were picked, the return value is null.
- @override
- Future<XFile> getImage({
- required ImageSource source,
- double? maxWidth,
- double? maxHeight,
- int? imageQuality,
- CameraDevice preferredCameraDevice = CameraDevice.rear,
+ ImagePickerOptions options = const ImagePickerOptions(),
}) async {
final String? capture =
- computeCaptureAttribute(source, preferredCameraDevice);
+ computeCaptureAttribute(source, options.preferredCameraDevice);
final List<XFile> files = await getFiles(
accept: _kAcceptImageMimeType,
capture: capture,
);
- return _imageResizer.resizeImageIfNeeded(
- files.first,
- maxWidth,
- maxHeight,
- imageQuality,
+ return files.isEmpty
+ ? null
+ : _imageResizer.resizeImageIfNeeded(
+ files.first,
+ options.maxWidth,
+ options.maxHeight,
+ options.imageQuality,
+ );
+ }
+
+ /// Returns a [List<XFile>] with the images that were picked, if any.
+ @override
+ Future<List<XFile>> getMultiImageWithOptions({
+ MultiImagePickerOptions options = const MultiImagePickerOptions(),
+ }) async {
+ final List<XFile> images = await getFiles(
+ accept: _kAcceptImageMimeType,
+ multiple: true,
);
+ final Iterable<Future<XFile>> resized = images.map(
+ (XFile image) => _imageResizer.resizeImageIfNeeded(
+ image,
+ options.imageOptions.maxWidth,
+ options.imageOptions.maxHeight,
+ options.imageOptions.imageQuality,
+ ),
+ );
+
+ return Future.wait<XFile>(resized);
}
/// Returns an [XFile] containing the video that was picked.
@@ -153,7 +98,7 @@
///
/// If no images were picked, the return value is null.
@override
- Future<XFile> getVideo({
+ Future<XFile?> getVideo({
required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Duration? maxDuration,
@@ -164,30 +109,7 @@
accept: _kAcceptVideoMimeType,
capture: capture,
);
- return files.first;
- }
-
- /// Injects a file input, and returns a list of XFile images that the user selected locally.
- @override
- Future<List<XFile>> getMultiImage({
- double? maxWidth,
- double? maxHeight,
- int? imageQuality,
- }) async {
- final List<XFile> images = await getFiles(
- accept: _kAcceptImageMimeType,
- multiple: true,
- );
- final Iterable<Future<XFile>> resized = images.map(
- (XFile image) => _imageResizer.resizeImageIfNeeded(
- image,
- maxWidth,
- maxHeight,
- imageQuality,
- ),
- );
-
- return Future.wait<XFile>(resized);
+ return files.isEmpty ? null : files.first;
}
/// Injects a file input, and returns a list of XFile media that the user selected locally.
@@ -239,6 +161,58 @@
return _getSelectedXFiles(input);
}
+ // Deprecated methods follow...
+
+ /// Returns an [XFile] with the image that was picked.
+ ///
+ /// The `source` argument controls where the image comes from. This can
+ /// be either [ImageSource.camera] or [ImageSource.gallery].
+ ///
+ /// Note that the `maxWidth`, `maxHeight` and `imageQuality` arguments are not supported on the web. If any of these arguments is supplied, it'll be silently ignored by the web version of the plugin.
+ ///
+ /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
+ /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
+ /// Defaults to [CameraDevice.rear].
+ ///
+ /// If no images were picked, the return value is null.
+ @override
+ @Deprecated('Use getImageFromSource instead.')
+ Future<XFile?> getImage({
+ required ImageSource source,
+ double? maxWidth,
+ double? maxHeight,
+ int? imageQuality,
+ CameraDevice preferredCameraDevice = CameraDevice.rear,
+ }) async {
+ return getImageFromSource(
+ source: source,
+ options: ImagePickerOptions(
+ maxWidth: maxWidth,
+ maxHeight: maxHeight,
+ imageQuality: imageQuality,
+ preferredCameraDevice: preferredCameraDevice,
+ ));
+ }
+
+ /// Injects a file input, and returns a list of XFile images that the user selected locally.
+ @override
+ @Deprecated('Use getMultiImageWithOptions instead.')
+ Future<List<XFile>> getMultiImage({
+ double? maxWidth,
+ double? maxHeight,
+ int? imageQuality,
+ }) async {
+ return getMultiImageWithOptions(
+ options: MultiImagePickerOptions(
+ imageOptions: ImageOptions(
+ maxWidth: maxWidth,
+ maxHeight: maxHeight,
+ imageQuality: imageQuality,
+ ),
+ ),
+ );
+ }
+
// DOM methods
/// Converts plugin configuration into a proper value for the `capture` attribute.
@@ -267,29 +241,6 @@
return input == null ? null : _getFilesFromInput(input);
}
- /// Monitors an <input type="file"> and returns the selected file.
- Future<PickedFile> _getSelectedFile(html.FileUploadInputElement input) {
- final Completer<PickedFile> completer = Completer<PickedFile>();
- // Observe the input until we can return something
- input.onChange.first.then((html.Event event) {
- final List<html.File>? files = _handleOnChangeEvent(event);
- if (!completer.isCompleted && files != null) {
- completer.complete(PickedFile(
- html.Url.createObjectUrl(files.first),
- ));
- }
- });
- input.onError.first.then((html.Event event) {
- if (!completer.isCompleted) {
- completer.completeError(event);
- }
- });
- // Note that we don't bother detaching from these streams, since the
- // "input" gets re-created in the DOM every time the user needs to
- // pick a file.
- return completer.future;
- }
-
/// Monitors an <input type="file"> and returns the selected file(s).
Future<List<XFile>> _getSelectedXFiles(html.FileUploadInputElement input) {
final Completer<List<XFile>> completer = Completer<List<XFile>>();
@@ -310,6 +261,11 @@
}).toList());
}
});
+
+ input.addEventListener('cancel', (html.Event _) {
+ completer.complete(<XFile>[]);
+ });
+
input.onError.first.then((html.Event event) {
if (!completer.isCompleted) {
completer.completeError(event);
@@ -361,6 +317,7 @@
void _injectAndActivate(html.Element element) {
_target.children.clear();
_target.children.add(element);
+ // TODO(dit): Reimplement this with the showPicker() API, https://github.com/flutter/flutter/issues/130365
element.click();
}
}
diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml
index a61a5b8..d7e274b 100644
--- a/packages/image_picker/image_picker_for_web/pubspec.yaml
+++ b/packages/image_picker/image_picker_for_web/pubspec.yaml
@@ -2,7 +2,7 @@
description: Web platform implementation of image_picker
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_for_web
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
-version: 2.2.0
+version: 3.0.0
environment:
sdk: ">=2.18.0 <4.0.0"
@@ -21,7 +21,7 @@
sdk: flutter
flutter_web_plugins:
sdk: flutter
- image_picker_platform_interface: ^2.8.0
+ image_picker_platform_interface: ^2.9.0
mime: ^1.0.4
dev_dependencies: