[image_picker] Multiple image support (#3783)

diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md
index d9127e2..7147c0f 100644
--- a/packages/image_picker/image_picker/CHANGELOG.md
+++ b/packages/image_picker/image_picker/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 0.8.1
+
+* Add a new method `getMultiImage` to allow picking multiple images on iOS 14 or higher
+and Android 4.3 or higher. Returns only 1 image for lower versions of iOS and Android.
+* Known issue: On Android, `getLostData` will only get the last picked image when picking multiple images,
+see: [#84634](https://github.com/flutter/flutter/issues/84634).
+
 ## 0.8.0+4
 
 * Cleaned up the README example
@@ -49,7 +56,7 @@
 
 ## 0.7.3
 
-* Endorse image_picker_for_web
+* Endorse image_picker_for_web.
 
 ## 0.7.2+1
 
@@ -57,7 +64,7 @@
 
 ## 0.7.2
 
-* Run CocoaPods iOS tests in RunnerUITests target
+* Run CocoaPods iOS tests in RunnerUITests target.
 
 ## 0.7.1
 
diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md
index 10899e2..3b3746d 100755
--- a/packages/image_picker/image_picker/README.md
+++ b/packages/image_picker/image_picker/README.md
@@ -11,6 +11,9 @@
 
 ### iOS
 
+Starting with version **0.8.1** the iOS implementation uses PHPicker to pick (multiple) images on iOS 14 or higher.
+As a result of implementing PHPicker it becomes impossible to pick HEIC images on the iOS simulator in iOS 14+. This is a known issue. Please test this on a real device, or test with non-HEIC images until Apple solves this issue.[63426347 - Apple known issue](https://www.google.com/search?q=63426347+apple&sxsrf=ALeKk01YnTMid5S0PYvhL8GbgXJ40ZS[…]t=gws-wiz&ved=0ahUKEwjKh8XH_5HwAhWL_rsIHUmHDN8Q4dUDCA8&uact=5) 
+
 Add the following keys to your _Info.plist_ file, located in `<project root>/ios/Runner/Info.plist`:
 
 * `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor.
@@ -19,6 +22,8 @@
 
 ### Android
 
+Starting with version **0.8.1** the Android implementation support to pick (multiple) images on Android 4.3 or higher.
+
 No configuration required - the plugin should work out of the box.
 
 It is no longer required to add `android:requestLegacyExternalStorage="true"` as an attribute to the `<application>` tag in AndroidManifest.xml, as `image_picker` has been updated to make use of scoped storage.
@@ -63,6 +68,8 @@
 
 There's no way to detect when this happens, so calling this method at the right place is essential. We recommend to wire this into some kind of start up check. Please refer to the example app to see how we used it.
 
+On Android, `getLostData` will only get the last picked image when picking multiple images, see: [#84634](https://github.com/flutter/flutter/issues/84634).
+
 ## Deprecation warnings in `pickImage`, `pickVideo` and `LostDataResponse`
 
 Starting with version **0.6.7** of the image_picker plugin, the API of the plugin changed slightly to allow for web implementations to exist.
diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
index c934b54..c4a686f 100644
--- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
+++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
@@ -22,6 +22,7 @@
 import io.flutter.plugin.common.PluginRegistry;
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
@@ -75,6 +76,7 @@
   @VisibleForTesting static final int REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY = 2342;
   @VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343;
   @VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345;
+  @VisibleForTesting static final int REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY = 2346;
   @VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352;
   @VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353;
   @VisibleForTesting static final int REQUEST_CAMERA_VIDEO_PERMISSION = 2355;
@@ -315,6 +317,15 @@
     launchPickImageFromGalleryIntent();
   }
 
+  public void chooseMultiImageFromGallery(MethodCall methodCall, MethodChannel.Result result) {
+    if (!setPendingMethodCallAndResult(methodCall, result)) {
+      finishWithAlreadyActiveError(result);
+      return;
+    }
+
+    launchMultiPickImageFromGalleryIntent();
+  }
+
   private void launchPickImageFromGalleryIntent() {
     Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
     pickImageIntent.setType("image/*");
@@ -322,6 +333,16 @@
     activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY);
   }
 
+  private void launchMultiPickImageFromGalleryIntent() {
+    Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+      pickImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+    }
+    pickImageIntent.setType("image/*");
+
+    activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY);
+  }
+
   public void takeImageWithCamera(MethodCall methodCall, MethodChannel.Result result) {
     if (!setPendingMethodCallAndResult(methodCall, result)) {
       finishWithAlreadyActiveError(result);
@@ -440,6 +461,9 @@
       case REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY:
         handleChooseImageResult(resultCode, data);
         break;
+      case REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY:
+        handleChooseMultiImageResult(resultCode, data);
+        break;
       case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA:
         handleCaptureImageResult(resultCode);
         break;
@@ -467,6 +491,24 @@
     finishWithSuccess(null);
   }
 
+  private void handleChooseMultiImageResult(int resultCode, Intent intent) {
+    if (resultCode == Activity.RESULT_OK && intent != null) {
+      ArrayList<String> paths = new ArrayList<>();
+      if (intent.getClipData() != null) {
+        for (int i = 0; i < intent.getClipData().getItemCount(); i++) {
+          paths.add(fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri()));
+        }
+      } else {
+        paths.add(fileUtils.getPathFromUri(activity, intent.getData()));
+      }
+      handleMultiImageResult(paths, false);
+      return;
+    }
+
+    // User cancelled choosing a picture.
+    finishWithSuccess(null);
+  }
+
   private void handleChooseVideoResult(int resultCode, Intent data) {
     if (resultCode == Activity.RESULT_OK && data != null) {
       String path = fileUtils.getPathFromUri(activity, data.getData());
@@ -516,26 +558,45 @@
     finishWithSuccess(null);
   }
 
+  private void handleMultiImageResult(
+      ArrayList<String> paths, boolean shouldDeleteOriginalIfScaled) {
+    if (methodCall != null) {
+      for (int i = 0; i < paths.size(); i++) {
+        String finalImagePath = getResizedImagePath(paths.get(i));
+
+        //delete original file if scaled
+        if (finalImagePath != null
+            && !finalImagePath.equals(paths.get(i))
+            && shouldDeleteOriginalIfScaled) {
+          new File(paths.get(i)).delete();
+        }
+        paths.set(i, finalImagePath);
+      }
+      finishWithListSuccess(paths);
+    }
+  }
+
   private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) {
     if (methodCall != null) {
-      Double maxWidth = methodCall.argument("maxWidth");
-      Double maxHeight = methodCall.argument("maxHeight");
-      Integer imageQuality = methodCall.argument("imageQuality");
-
-      String finalImagePath =
-          imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality);
-
-      finishWithSuccess(finalImagePath);
-
+      String finalImagePath = getResizedImagePath(path);
       //delete original file if scaled
       if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) {
         new File(path).delete();
       }
+      finishWithSuccess(finalImagePath);
     } else {
       finishWithSuccess(path);
     }
   }
 
+  private String getResizedImagePath(String path) {
+    Double maxWidth = methodCall.argument("maxWidth");
+    Double maxHeight = methodCall.argument("maxHeight");
+    Integer imageQuality = methodCall.argument("imageQuality");
+
+    return imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality);
+  }
+
   private void handleVideoResult(String path) {
     finishWithSuccess(path);
   }
@@ -564,6 +625,17 @@
     clearMethodCallAndResult();
   }
 
+  private void finishWithListSuccess(ArrayList<String> imagePaths) {
+    if (pendingResult == null) {
+      for (String imagePath : imagePaths) {
+        cache.saveResult(imagePath, null, null);
+      }
+      return;
+    }
+    pendingResult.success(imagePaths);
+    clearMethodCallAndResult();
+  }
+
   private void finishWithAlreadyActiveError(MethodChannel.Result result) {
     result.error("already_active", "Image picker is already active", null);
   }
diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
index bffc903..577675b 100644
--- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
+++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
@@ -91,6 +91,7 @@
   }
 
   static final String METHOD_CALL_IMAGE = "pickImage";
+  static final String METHOD_CALL_MULTI_IMAGE = "pickMultiImage";
   static final String METHOD_CALL_VIDEO = "pickVideo";
   private static final String METHOD_CALL_RETRIEVE = "retrieve";
   private static final int CAMERA_DEVICE_FRONT = 1;
@@ -302,6 +303,9 @@
             throw new IllegalArgumentException("Invalid image source: " + imageSource);
         }
         break;
+      case METHOD_CALL_MULTI_IMAGE:
+        delegate.chooseMultiImageFromGallery(call, result);
+        break;
       case METHOD_CALL_VIDEO:
         imageSource = call.argument("source");
         switch (imageSource) {
diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java
index da53b10..f8be668 100644
--- a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java
+++ b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java
@@ -104,6 +104,15 @@
   }
 
   @Test
+  public void chooseMultiImageFromGallery_WhenPendingResultExists_FinishesWithAlreadyActiveError() {
+    ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+    delegate.chooseMultiImageFromGallery(mockMethodCall, mockResult);
+
+    verifyFinishedWithAlreadyActiveError();
+    verifyNoMoreInteractions(mockResult);
+  }
+
   public void
       chooseImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
     when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
index a0ce87f..422b8be 100644
--- a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
+++ b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
@@ -32,6 +32,7 @@
   private static final int SOURCE_CAMERA = 0;
   private static final int SOURCE_GALLERY = 1;
   private static final String PICK_IMAGE = "pickImage";
+  private static final String PICK_MULTI_IMAGE = "pickMultiImage";
   private static final String PICK_VIDEO = "pickVideo";
 
   @Rule public ExpectedException exception = ExpectedException.none();
@@ -93,6 +94,14 @@
   }
 
   @Test
+  public void onMethodCall_InvokesChooseMultiImageFromGallery() {
+    MethodCall call = buildMethodCall(PICK_MULTI_IMAGE);
+    plugin.onMethodCall(call, mockResult);
+    verify(mockImagePickerDelegate).chooseMultiImageFromGallery(eq(call), any());
+    verifyZeroInteractions(mockResult);
+  }
+
+  @Test
   public void onMethodCall_WhenSourceIsCamera_InvokesTakeImageWithCamera() {
     MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA);
     plugin.onMethodCall(call, mockResult);
@@ -173,4 +182,8 @@
 
     return new MethodCall(method, arguments);
   }
+
+  private MethodCall buildMethodCall(String method) {
+    return new MethodCall(method, null);
+  }
 }
diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m
index 04ba4b9..f667526 100644
--- a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m
+++ b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m
@@ -22,7 +22,7 @@
 
 @interface FLTImagePickerPlugin (Test)
 @property(copy, nonatomic) FlutterResult result;
-- (void)handleSavedPath:(NSString *)path;
+- (void)handleSavedPathList:(NSMutableArray *)pathList;
 - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker;
 @end
 
@@ -122,21 +122,6 @@
   XCTAssertEqual([plugin getImagePickerController].videoMaximumDuration, 95);
 }
 
-- (void)testPluginPickImageSelectMultipleTimes {
-  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
-  FlutterMethodCall *call =
-      [FlutterMethodCall methodCallWithMethodName:@"pickImage"
-                                        arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}];
-  [plugin handleMethodCall:call
-                    result:^(id _Nullable r){
-                    }];
-  plugin.result = ^(id result) {
-
-  };
-  [plugin handleSavedPath:@"test"];
-  [plugin handleSavedPath:@"test"];
-}
-
 - (void)testViewController {
   UIWindow *window = [UIWindow new];
   MockViewController *vc1 = [MockViewController new];
@@ -149,4 +134,62 @@
   XCTAssertEqual([plugin viewControllerWithWindow:window], vc2);
 }
 
+- (void)testPluginMultiImagePathIsNil {
+  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
+
+  dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
+  __block FlutterError *pickImageResult = nil;
+
+  plugin.result = ^(id _Nullable r) {
+    pickImageResult = r;
+    dispatch_semaphore_signal(resultSemaphore);
+  };
+  [plugin handleSavedPathList:nil];
+
+  dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
+
+  XCTAssertEqualObjects(pickImageResult.code, @"create_error");
+}
+
+- (void)testPluginMultiImagePathHasNullItem {
+  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
+  NSMutableArray *pathList = [NSMutableArray new];
+
+  [pathList addObject:[NSNull null]];
+
+  dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
+  __block FlutterError *pickImageResult = nil;
+
+  plugin.result = ^(id _Nullable r) {
+    pickImageResult = r;
+    dispatch_semaphore_signal(resultSemaphore);
+  };
+  [plugin handleSavedPathList:pathList];
+
+  dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
+
+  XCTAssertEqualObjects(pickImageResult.code, @"create_error");
+}
+
+- (void)testPluginMultiImagePathHasItem {
+  FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
+  NSString *savedPath = @"test";
+  NSMutableArray *pathList = [NSMutableArray new];
+
+  [pathList addObject:savedPath];
+
+  dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
+  __block id pickImageResult = nil;
+
+  plugin.result = ^(id _Nullable r) {
+    pickImageResult = r;
+    dispatch_semaphore_signal(resultSemaphore);
+  };
+  [plugin handleSavedPathList:pathList];
+
+  dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
+
+  XCTAssertEqual(pickImageResult, pathList);
+}
+
 @end
diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m
index e3df641..7c91606 100644
--- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m
+++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m
@@ -14,6 +14,7 @@
 #import "FLTImagePickerImageUtil.h"
 #import "FLTImagePickerMetaDataUtil.h"
 #import "FLTImagePickerPhotoAssetUtil.h"
+#import "FLTPHPickerSaveImageToPathOperation.h"
 
 @interface FLTImagePickerPlugin () <UINavigationControllerDelegate,
                                     UIImagePickerControllerDelegate,
@@ -21,6 +22,8 @@
 
 @property(copy, nonatomic) FlutterResult result;
 
+@property(assign, nonatomic) int maxImagesAllowed;
+
 @property(copy, nonatomic) NSDictionary *arguments;
 
 @property(strong, nonatomic) PHPickerViewController *pickerViewController API_AVAILABLE(ios(14));
@@ -67,17 +70,17 @@
   return topController;
 }
 
-- (void)pickImageWithPHPicker:(bool)single API_AVAILABLE(ios(14)) {
+- (void)pickImageWithPHPicker:(int)maxImagesAllowed API_AVAILABLE(ios(14)) {
   PHPickerConfiguration *config =
       [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary];
-  if (!single) {
-    config.selectionLimit = 0;  // Setting to zero allow us to pick unlimited photos
-  }
+  config.selectionLimit = maxImagesAllowed;  // Setting to zero allow us to pick unlimited photos
   config.filter = [PHPickerFilter imagesFilter];
 
   _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config];
   _pickerViewController.delegate = self;
 
+  self.maxImagesAllowed = maxImagesAllowed;
+
   [self checkPhotoAuthorizationForAccessLevel];
 }
 
@@ -89,6 +92,8 @@
 
   int imageSource = [[_arguments objectForKey:@"source"] intValue];
 
+  self.maxImagesAllowed = 1;
+
   switch (imageSource) {
     case SOURCE_CAMERA: {
       NSInteger cameraDevice = [[_arguments objectForKey:@"cameraDevice"] intValue];
@@ -124,7 +129,7 @@
     if (imageSource == SOURCE_GALLERY) {  // Capture is not possible with PHPicker
       if (@available(iOS 14, *)) {
         // PHPicker is used
-        [self pickImageWithPHPicker:true];
+        [self pickImageWithPHPicker:1];
       } else {
         // UIImagePicker is used
         [self pickImageWithUIImagePicker];
@@ -136,7 +141,9 @@
     if (@available(iOS 14, *)) {
       self.result = result;
       _arguments = call.arguments;
-      [self pickImageWithPHPicker:false];
+      [self pickImageWithPHPicker:0];
+    } else {
+      [self pickImageWithUIImagePicker];
     }
   } else if ([@"pickVideo" isEqualToString:call.method]) {
     _imagePickerController = [[UIImagePickerController alloc] init];
@@ -358,54 +365,55 @@
 - (void)picker:(PHPickerViewController *)picker
     didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14)) {
   [picker dismissViewControllerAnimated:YES completion:nil];
+  dispatch_queue_t backgroundQueue =
+      dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
+  dispatch_async(backgroundQueue, ^{
+    if (results.count == 0) {
+      self.result(nil);
+      self.result = nil;
+      self->_arguments = nil;
+      return;
+    }
+    NSNumber *maxWidth = [self->_arguments objectForKey:@"maxWidth"];
+    NSNumber *maxHeight = [self->_arguments objectForKey:@"maxHeight"];
+    NSNumber *imageQuality = [self->_arguments objectForKey:@"imageQuality"];
+    NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality];
+    NSOperationQueue *operationQueue = [NSOperationQueue new];
+    NSMutableArray *pathList = [self createNSMutableArrayWithSize:results.count];
 
-  NSNumber *maxWidth = [_arguments objectForKey:@"maxWidth"];
-  NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"];
-  NSNumber *imageQuality = [_arguments objectForKey:@"imageQuality"];
-  NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality];
-
-  for (PHPickerResult *result in results) {
-    [result.itemProvider
-        loadObjectOfClass:[UIImage class]
-        completionHandler:^(__kindof id<NSItemProviderReading> _Nullable image,
-                            NSError *_Nullable error) {
-          if ([image isKindOfClass:[UIImage class]]) {
-            __block UIImage *localImage = image;
-            dispatch_async(dispatch_get_main_queue(), ^{
-              PHAsset *originalAsset =
-                  [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:result];
-
-              if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) {
-                localImage = [FLTImagePickerImageUtil scaledImage:localImage
-                                                         maxWidth:maxWidth
-                                                        maxHeight:maxHeight
-                                              isMetadataAvailable:originalAsset != nil];
-              }
-
-              if (!originalAsset) {
-                // Image picked without an original asset (e.g. User took a photo directly)
-                [self saveImageWithPickerInfo:nil
-                                        image:localImage
-                                 imageQuality:desiredImageQuality];
-              } else {
-                [[PHImageManager defaultManager]
-                    requestImageDataForAsset:originalAsset
-                                     options:nil
-                               resultHandler:^(
-                                   NSData *_Nullable imageData, NSString *_Nullable dataUTI,
-                                   UIImageOrientation orientation, NSDictionary *_Nullable info) {
-                                 // maxWidth and maxHeight are used only for GIF images.
-                                 [self saveImageWithOriginalImageData:imageData
-                                                                image:localImage
-                                                             maxWidth:maxWidth
+    for (int i = 0; i < results.count; i++) {
+      PHPickerResult *result = results[i];
+      FLTPHPickerSaveImageToPathOperation *operation =
+          [[FLTPHPickerSaveImageToPathOperation alloc] initWithResult:result
                                                             maxHeight:maxHeight
-                                                         imageQuality:desiredImageQuality];
-                               }];
-              }
-            });
-          }
-        }];
-  }
+                                                             maxWidth:maxWidth
+                                                  desiredImageQuality:desiredImageQuality
+                                                       savedPathBlock:^(NSString *savedPath) {
+                                                         pathList[i] = savedPath;
+                                                       }];
+      [operationQueue addOperation:operation];
+    }
+    [operationQueue waitUntilAllOperationsAreFinished];
+    dispatch_async(dispatch_get_main_queue(), ^{
+      [self handleSavedPathList:pathList];
+    });
+  });
+}
+
+/**
+ * Creates an NSMutableArray of a certain size filled with NSNull objects.
+ *
+ * The difference with initWithCapacity is that initWithCapacity still gives an empty array making
+ * it impossible to add objects on an index larger than the size.
+ *
+ * @param @size The length of the required array
+ * @return @NSMutableArray An array of a specified size
+ */
+- (NSMutableArray *)createNSMutableArrayWithSize:(NSUInteger)size {
+  NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:size];
+  for (int i = 0; i < size; [mutableArray addObject:[NSNull null]], i++)
+    ;
+  return mutableArray;
 }
 
 - (void)imagePickerController:(UIImagePickerController *)picker
@@ -504,7 +512,7 @@
                                                           maxWidth:maxWidth
                                                          maxHeight:maxHeight
                                                       imageQuality:imageQuality];
-  [self handleSavedPath:savedPath];
+  [self handleSavedPathList:@[ savedPath ]];
 }
 
 - (void)saveImageWithPickerInfo:(NSDictionary *)info
@@ -513,18 +521,43 @@
   NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info
                                                                         image:image
                                                                  imageQuality:imageQuality];
-  [self handleSavedPath:savedPath];
+  [self handleSavedPathList:@[ savedPath ]];
 }
 
-- (void)handleSavedPath:(NSString *)path {
+/**
+ * Applies NSMutableArray on the FLutterResult.
+ *
+ * NSString must be returned by FlutterResult if the single image
+ * mode is active. It is checked by @c maxImagesAllowed and
+ * returns the first object of the @c pathlist.
+ *
+ * NSMutableArray must be returned by FlutterResult if the multi-image
+ * mode is active. After the @c pathlist count is checked then it returns
+ * the @c pathlist.
+ *
+ * @param @pathList that should be applied to FlutterResult.
+ */
+- (void)handleSavedPathList:(NSArray *)pathList {
   if (!self.result) {
     return;
   }
-  if (path) {
-    self.result(path);
+
+  if (pathList) {
+    if (![pathList containsObject:[NSNull null]]) {
+      if ((self.maxImagesAllowed == 1)) {
+        self.result(pathList.firstObject);
+      } else {
+        self.result(pathList);
+      }
+    } else {
+      self.result([FlutterError errorWithCode:@"create_error"
+                                      message:@"pathList's items should not be null"
+                                      details:nil]);
+    }
   } else {
+    // This should never happen.
     self.result([FlutterError errorWithCode:@"create_error"
-                                    message:@"Temporary file could not be created"
+                                    message:@"pathList should not be nil"
                                     details:nil]);
   }
   self.result = nil;
diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h
new file mode 100644
index 0000000..7ba3d28
--- /dev/null
+++ b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h
@@ -0,0 +1,31 @@
+// 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 <Foundation/Foundation.h>
+#import <PhotosUI/PhotosUI.h>
+
+#import "FLTImagePickerImageUtil.h"
+#import "FLTImagePickerMetaDataUtil.h"
+#import "FLTImagePickerPhotoAssetUtil.h"
+
+/*!
+ @class FLTPHPickerSaveImageToPathOperation
+
+ @brief The FLTPHPickerSaveImageToPathOperation class
+
+ @discussion    This class was implemented to handle saved image paths and populate the pathList
+ with the final result by using GetSavedPath type block.
+
+ @superclass SuperClass: NSOperation\n
+ @helps It helps FLTImagePickerPlugin class.
+ */
+@interface FLTPHPickerSaveImageToPathOperation : NSOperation
+
+- (instancetype)initWithResult:(PHPickerResult *)result
+                     maxHeight:(NSNumber *)maxHeight
+                      maxWidth:(NSNumber *)maxWidth
+           desiredImageQuality:(NSNumber *)desiredImageQuality
+                savedPathBlock:(void (^)(NSString *))savedPathBlock API_AVAILABLE(ios(14));
+
+@end
diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m
new file mode 100644
index 0000000..30da227
--- /dev/null
+++ b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m
@@ -0,0 +1,132 @@
+// 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 "FLTPHPickerSaveImageToPathOperation.h"
+
+API_AVAILABLE(ios(14))
+@interface FLTPHPickerSaveImageToPathOperation ()
+
+@property(strong, nonatomic) PHPickerResult *result;
+@property(assign, nonatomic) NSNumber *maxHeight;
+@property(assign, nonatomic) NSNumber *maxWidth;
+@property(assign, nonatomic) NSNumber *desiredImageQuality;
+
+@end
+
+typedef void (^GetSavedPath)(NSString *);
+
+@implementation FLTPHPickerSaveImageToPathOperation {
+  BOOL executing;
+  BOOL finished;
+  GetSavedPath getSavedPath;
+}
+
+- (instancetype)initWithResult:(PHPickerResult *)result
+                     maxHeight:(NSNumber *)maxHeight
+                      maxWidth:(NSNumber *)maxWidth
+           desiredImageQuality:(NSNumber *)desiredImageQuality
+                savedPathBlock:(GetSavedPath)savedPathBlock API_AVAILABLE(ios(14)) {
+  if (self = [super init]) {
+    if (result) {
+      self.result = result;
+      self.maxHeight = maxHeight;
+      self.maxWidth = maxWidth;
+      self.desiredImageQuality = desiredImageQuality;
+      getSavedPath = savedPathBlock;
+      executing = NO;
+      finished = NO;
+    } else {
+      return nil;
+    }
+    return self;
+  } else {
+    return nil;
+  }
+}
+
+- (BOOL)isConcurrent {
+  return YES;
+}
+
+- (BOOL)isExecuting {
+  return executing;
+}
+
+- (BOOL)isFinished {
+  return finished;
+}
+
+- (void)setFinished:(BOOL)isFinished {
+  [self willChangeValueForKey:@"isFinished"];
+  self->finished = isFinished;
+  [self didChangeValueForKey:@"isFinished"];
+}
+
+- (void)setExecuting:(BOOL)isExecuting {
+  [self willChangeValueForKey:@"isExecuting"];
+  self->executing = isExecuting;
+  [self didChangeValueForKey:@"isExecuting"];
+}
+
+- (void)completeOperationWithPath:(NSString *)savedPath {
+  [self setExecuting:NO];
+  [self setFinished:YES];
+  getSavedPath(savedPath);
+}
+
+- (void)start {
+  if ([self isCancelled]) {
+    [self setFinished:YES];
+    return;
+  }
+  if (@available(iOS 14, *)) {
+    [self setExecuting:YES];
+    [self.result.itemProvider
+        loadObjectOfClass:[UIImage class]
+        completionHandler:^(__kindof id<NSItemProviderReading> _Nullable image,
+                            NSError *_Nullable error) {
+          if ([image isKindOfClass:[UIImage class]]) {
+            __block UIImage *localImage = image;
+            PHAsset *originalAsset =
+                [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result];
+
+            if (self.maxWidth != (id)[NSNull null] || self.maxHeight != (id)[NSNull null]) {
+              localImage = [FLTImagePickerImageUtil scaledImage:localImage
+                                                       maxWidth:self.maxWidth
+                                                      maxHeight:self.maxHeight
+                                            isMetadataAvailable:originalAsset != nil];
+            }
+            __block NSString *savedPath;
+            if (!originalAsset) {
+              // Image picked without an original asset (e.g. User pick image without permission)
+              savedPath =
+                  [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil
+                                                                  image:localImage
+                                                           imageQuality:self.desiredImageQuality];
+              [self completeOperationWithPath:savedPath];
+            } else {
+              [[PHImageManager defaultManager]
+                  requestImageDataForAsset:originalAsset
+                                   options:nil
+                             resultHandler:^(
+                                 NSData *_Nullable imageData, NSString *_Nullable dataUTI,
+                                 UIImageOrientation orientation, NSDictionary *_Nullable info) {
+                               // maxWidth and maxHeight are used only for GIF images.
+                               savedPath = [FLTImagePickerPhotoAssetUtil
+                                   saveImageWithOriginalImageData:imageData
+                                                            image:localImage
+                                                         maxWidth:self.maxWidth
+                                                        maxHeight:self.maxHeight
+                                                     imageQuality:self.desiredImageQuality];
+                               [self completeOperationWithPath:savedPath];
+                             }];
+            }
+          }
+        }];
+  } else {
+    [self setFinished:YES];
+  }
+}
+
+@end
diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart
index 77c26d4..f4dee93 100755
--- a/packages/image_picker/image_picker/lib/image_picker.dart
+++ b/packages/image_picker/image_picker/lib/image_picker.dart
@@ -54,6 +54,8 @@
   ///
   /// 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.
+  ///
+  /// See also [getMultiImage] to allow users to select multiple images at once.
   Future<PickedFile?> getImage({
     required ImageSource source,
     double? maxWidth,
@@ -70,6 +72,37 @@
     );
   }
 
+  /// Returns a [List<PickedFile>] object wrapping the images that were picked.
+  ///
+  /// The returned [List<PickedFile>] is intended to be used within a single APP session. Do not save the file path and use it across sessions.
+  ///
+  /// 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.
+  ///
+  /// This method is not supported in iOS versions lower than 14.
+  ///
+  /// If specified, the images will be at most `maxWidth` wide and
+  /// `maxHeight` tall. Otherwise the images will be returned at it's
+  /// original width and height.
+  /// The `imageQuality` argument modifies the quality of the images, ranging from 0-100
+  /// where 100 is the original/max quality. If `imageQuality` is null, the images with
+  /// the original quality will be returned. Compression is only supported for certain
+  /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked,
+  /// a warning message will be logged.
+  ///
+  /// See also [getImage] to allow users to only pick a single image.
+  Future<List<PickedFile>?> getMultiImage({
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+  }) {
+    return platform.pickMultiImage(
+      maxWidth: maxWidth,
+      maxHeight: maxHeight,
+      imageQuality: imageQuality,
+    );
+  }
+
   /// Returns a [PickedFile] object wrapping the video that was picked.
   ///
   /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions.
diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml
index 4dc7785..b8aa933 100755
--- a/packages/image_picker/image_picker/pubspec.yaml
+++ b/packages/image_picker/image_picker/pubspec.yaml
@@ -3,7 +3,7 @@
   library, and taking new pictures with the camera.
 repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
-version: 0.8.0+4
+version: 0.8.1
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
@@ -24,8 +24,8 @@
   flutter:
     sdk: flutter
   flutter_plugin_android_lifecycle: ^2.0.1
-  image_picker_platform_interface: ^2.0.0
   image_picker_for_web: ^2.0.0
+  image_picker_platform_interface: ^2.1.0
 
 dev_dependencies:
   flutter_test:
diff --git a/packages/image_picker/image_picker/test/image_picker_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart
index f56d47f..d83b403 100644
--- a/packages/image_picker/image_picker/test/image_picker_test.dart
+++ b/packages/image_picker/image_picker/test/image_picker_test.dart
@@ -20,15 +20,6 @@
 
     final picker = ImagePicker();
 
-    setUp(() {
-      channel.setMockMethodCallHandler((MethodCall methodCall) async {
-        log.add(methodCall);
-        return '';
-      });
-
-      log.clear();
-    });
-
     test('ImagePicker platform instance overrides the actual platform used',
         () {
       final ImagePickerPlatform savedPlatform = ImagePickerPlatform.instance;
@@ -38,312 +29,420 @@
       ImagePickerPlatform.instance = savedPlatform;
     });
 
-    group('#pickImage', () {
-      test('passes the image source argument correctly', () async {
-        await picker.getImage(source: ImageSource.camera);
-        await picker.getImage(source: ImageSource.gallery);
+    group('#Single image/video', () {
+      setUp(() {
+        channel.setMockMethodCallHandler((MethodCall methodCall) async {
+          log.add(methodCall);
+          return '';
+        });
 
-        expect(
-          log,
-          <Matcher>[
-            isMethodCall('pickImage', arguments: <String, dynamic>{
-              'source': 0,
-              'maxWidth': null,
-              'maxHeight': null,
-              'imageQuality': null,
-              'cameraDevice': 0
-            }),
-            isMethodCall('pickImage', arguments: <String, dynamic>{
-              'source': 1,
-              'maxWidth': null,
-              'maxHeight': null,
-              'imageQuality': null,
-              'cameraDevice': 0
-            }),
-          ],
-        );
+        log.clear();
       });
 
-      test('passes the width and height arguments correctly', () async {
-        await picker.getImage(source: ImageSource.camera);
-        await picker.getImage(
-          source: ImageSource.camera,
-          maxWidth: 10.0,
-        );
-        await picker.getImage(
-          source: ImageSource.camera,
-          maxHeight: 10.0,
-        );
-        await picker.getImage(
-          source: ImageSource.camera,
-          maxWidth: 10.0,
-          maxHeight: 20.0,
-        );
-        await picker.getImage(
-            source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70);
-        await picker.getImage(
-            source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70);
-        await picker.getImage(
+      group('#pickImage', () {
+        test('passes the image source argument correctly', () async {
+          await picker.getImage(source: ImageSource.camera);
+          await picker.getImage(source: ImageSource.gallery);
+
+          expect(
+            log,
+            <Matcher>[
+              isMethodCall('pickImage', arguments: <String, dynamic>{
+                'source': 0,
+                'maxWidth': null,
+                'maxHeight': null,
+                'imageQuality': null,
+                'cameraDevice': 0
+              }),
+              isMethodCall('pickImage', arguments: <String, dynamic>{
+                'source': 1,
+                'maxWidth': null,
+                'maxHeight': null,
+                'imageQuality': null,
+                'cameraDevice': 0
+              }),
+            ],
+          );
+        });
+
+        test('passes the width and height arguments correctly', () async {
+          await picker.getImage(source: ImageSource.camera);
+          await picker.getImage(
+            source: ImageSource.camera,
+            maxWidth: 10.0,
+          );
+          await picker.getImage(
+            source: ImageSource.camera,
+            maxHeight: 10.0,
+          );
+          await picker.getImage(
             source: ImageSource.camera,
             maxWidth: 10.0,
             maxHeight: 20.0,
-            imageQuality: 70);
+          );
+          await picker.getImage(
+              source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70);
+          await picker.getImage(
+              source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70);
+          await picker.getImage(
+              source: ImageSource.camera,
+              maxWidth: 10.0,
+              maxHeight: 20.0,
+              imageQuality: 70);
 
-        expect(
-          log,
-          <Matcher>[
-            isMethodCall('pickImage', arguments: <String, dynamic>{
-              'source': 0,
-              'maxWidth': null,
-              'maxHeight': null,
-              'imageQuality': null,
-              'cameraDevice': 0
-            }),
-            isMethodCall('pickImage', arguments: <String, dynamic>{
-              'source': 0,
-              'maxWidth': 10.0,
-              'maxHeight': null,
-              'imageQuality': null,
-              'cameraDevice': 0
-            }),
-            isMethodCall('pickImage', arguments: <String, dynamic>{
-              'source': 0,
-              'maxWidth': null,
-              'maxHeight': 10.0,
-              'imageQuality': null,
-              'cameraDevice': 0
-            }),
-            isMethodCall('pickImage', arguments: <String, dynamic>{
-              'source': 0,
-              'maxWidth': 10.0,
-              'maxHeight': 20.0,
-              'imageQuality': null,
-              'cameraDevice': 0
-            }),
-            isMethodCall('pickImage', arguments: <String, dynamic>{
-              'source': 0,
-              'maxWidth': 10.0,
-              'maxHeight': null,
-              'imageQuality': 70,
-              'cameraDevice': 0
-            }),
-            isMethodCall('pickImage', arguments: <String, dynamic>{
-              'source': 0,
-              'maxWidth': null,
-              'maxHeight': 10.0,
-              'imageQuality': 70,
-              'cameraDevice': 0
-            }),
-            isMethodCall('pickImage', arguments: <String, dynamic>{
-              'source': 0,
-              'maxWidth': 10.0,
-              'maxHeight': 20.0,
-              'imageQuality': 70,
-              'cameraDevice': 0
-            }),
-          ],
-        );
+          expect(
+            log,
+            <Matcher>[
+              isMethodCall('pickImage', arguments: <String, dynamic>{
+                'source': 0,
+                'maxWidth': null,
+                'maxHeight': null,
+                'imageQuality': null,
+                'cameraDevice': 0
+              }),
+              isMethodCall('pickImage', arguments: <String, dynamic>{
+                'source': 0,
+                'maxWidth': 10.0,
+                'maxHeight': null,
+                'imageQuality': null,
+                'cameraDevice': 0
+              }),
+              isMethodCall('pickImage', arguments: <String, dynamic>{
+                'source': 0,
+                'maxWidth': null,
+                'maxHeight': 10.0,
+                'imageQuality': null,
+                'cameraDevice': 0
+              }),
+              isMethodCall('pickImage', arguments: <String, dynamic>{
+                'source': 0,
+                'maxWidth': 10.0,
+                'maxHeight': 20.0,
+                'imageQuality': null,
+                'cameraDevice': 0
+              }),
+              isMethodCall('pickImage', arguments: <String, dynamic>{
+                'source': 0,
+                'maxWidth': 10.0,
+                'maxHeight': null,
+                'imageQuality': 70,
+                'cameraDevice': 0
+              }),
+              isMethodCall('pickImage', arguments: <String, dynamic>{
+                'source': 0,
+                'maxWidth': null,
+                'maxHeight': 10.0,
+                'imageQuality': 70,
+                'cameraDevice': 0
+              }),
+              isMethodCall('pickImage', arguments: <String, dynamic>{
+                'source': 0,
+                'maxWidth': 10.0,
+                'maxHeight': 20.0,
+                'imageQuality': 70,
+                'cameraDevice': 0
+              }),
+            ],
+          );
+        });
+
+        test('does not accept a negative width or height argument', () {
+          expect(
+            picker.getImage(source: ImageSource.camera, maxWidth: -1.0),
+            throwsArgumentError,
+          );
+
+          expect(
+            picker.getImage(source: ImageSource.camera, maxHeight: -1.0),
+            throwsArgumentError,
+          );
+        });
+
+        test('handles a null image path response gracefully', () async {
+          channel.setMockMethodCallHandler((MethodCall methodCall) => null);
+
+          expect(await picker.getImage(source: ImageSource.gallery), isNull);
+          expect(await picker.getImage(source: ImageSource.camera), isNull);
+        });
+
+        test('camera position defaults to back', () async {
+          await picker.getImage(source: ImageSource.camera);
+
+          expect(
+            log,
+            <Matcher>[
+              isMethodCall('pickImage', arguments: <String, dynamic>{
+                'source': 0,
+                'maxWidth': null,
+                'maxHeight': null,
+                'imageQuality': null,
+                'cameraDevice': 0,
+              }),
+            ],
+          );
+        });
+
+        test('camera position can set to front', () async {
+          await picker.getImage(
+              source: ImageSource.camera,
+              preferredCameraDevice: CameraDevice.front);
+
+          expect(
+            log,
+            <Matcher>[
+              isMethodCall('pickImage', arguments: <String, dynamic>{
+                'source': 0,
+                'maxWidth': null,
+                'maxHeight': null,
+                'imageQuality': null,
+                'cameraDevice': 1,
+              }),
+            ],
+          );
+        });
       });
 
-      test('does not accept a negative width or height argument', () {
-        expect(
-          picker.getImage(source: ImageSource.camera, maxWidth: -1.0),
-          throwsArgumentError,
-        );
+      group('#pickVideo', () {
+        test('passes the image source argument correctly', () async {
+          await picker.getVideo(source: ImageSource.camera);
+          await picker.getVideo(source: ImageSource.gallery);
 
-        expect(
-          picker.getImage(source: ImageSource.camera, maxHeight: -1.0),
-          throwsArgumentError,
-        );
+          expect(
+            log,
+            <Matcher>[
+              isMethodCall('pickVideo', arguments: <String, dynamic>{
+                'source': 0,
+                'cameraDevice': 0,
+                'maxDuration': null,
+              }),
+              isMethodCall('pickVideo', arguments: <String, dynamic>{
+                'source': 1,
+                'cameraDevice': 0,
+                'maxDuration': null,
+              }),
+            ],
+          );
+        });
+
+        test('passes the duration argument correctly', () async {
+          await picker.getVideo(source: ImageSource.camera);
+          await picker.getVideo(
+              source: ImageSource.camera,
+              maxDuration: const Duration(seconds: 10));
+          await picker.getVideo(
+              source: ImageSource.camera,
+              maxDuration: const Duration(minutes: 1));
+          await picker.getVideo(
+              source: ImageSource.camera,
+              maxDuration: const Duration(hours: 1));
+          expect(
+            log,
+            <Matcher>[
+              isMethodCall('pickVideo', arguments: <String, dynamic>{
+                'source': 0,
+                'maxDuration': null,
+                'cameraDevice': 0,
+              }),
+              isMethodCall('pickVideo', arguments: <String, dynamic>{
+                'source': 0,
+                'maxDuration': 10,
+                'cameraDevice': 0,
+              }),
+              isMethodCall('pickVideo', arguments: <String, dynamic>{
+                'source': 0,
+                'maxDuration': 60,
+                'cameraDevice': 0,
+              }),
+              isMethodCall('pickVideo', arguments: <String, dynamic>{
+                'source': 0,
+                'maxDuration': 3600,
+                'cameraDevice': 0,
+              }),
+            ],
+          );
+        });
+
+        test('handles a null video path response gracefully', () async {
+          channel.setMockMethodCallHandler((MethodCall methodCall) => null);
+
+          expect(await picker.getVideo(source: ImageSource.gallery), isNull);
+          expect(await picker.getVideo(source: ImageSource.camera), isNull);
+        });
+
+        test('camera position defaults to back', () async {
+          await picker.getVideo(source: ImageSource.camera);
+
+          expect(
+            log,
+            <Matcher>[
+              isMethodCall('pickVideo', arguments: <String, dynamic>{
+                'source': 0,
+                'cameraDevice': 0,
+                'maxDuration': null,
+              }),
+            ],
+          );
+        });
+
+        test('camera position can set to front', () async {
+          await picker.getVideo(
+              source: ImageSource.camera,
+              preferredCameraDevice: CameraDevice.front);
+
+          expect(
+            log,
+            <Matcher>[
+              isMethodCall('pickVideo', arguments: <String, dynamic>{
+                'source': 0,
+                'maxDuration': null,
+                'cameraDevice': 1,
+              }),
+            ],
+          );
+        });
       });
 
-      test('handles a null image path response gracefully', () async {
-        channel.setMockMethodCallHandler((MethodCall methodCall) => null);
+      group('#retrieveLostData', () {
+        test('retrieveLostData get success response', () async {
+          channel.setMockMethodCallHandler((MethodCall methodCall) async {
+            return <String, String>{
+              'type': 'image',
+              'path': '/example/path',
+            };
+          });
+          final LostData response = await picker.getLostData();
+          expect(response.type, RetrieveType.image);
+          expect(response.file!.path, '/example/path');
+        });
 
-        expect(await picker.getImage(source: ImageSource.gallery), isNull);
-        expect(await picker.getImage(source: ImageSource.camera), isNull);
-      });
+        test('retrieveLostData get error response', () async {
+          channel.setMockMethodCallHandler((MethodCall methodCall) async {
+            return <String, String>{
+              'type': 'video',
+              'errorCode': 'test_error_code',
+              'errorMessage': 'test_error_message',
+            };
+          });
+          final LostData response = await picker.getLostData();
+          expect(response.type, RetrieveType.video);
+          expect(response.exception!.code, 'test_error_code');
+          expect(response.exception!.message, 'test_error_message');
+        });
 
-      test('camera position defaults to back', () async {
-        await picker.getImage(source: ImageSource.camera);
+        test('retrieveLostData get null response', () async {
+          channel.setMockMethodCallHandler((MethodCall methodCall) async {
+            return null;
+          });
+          expect((await picker.getLostData()).isEmpty, true);
+        });
 
-        expect(
-          log,
-          <Matcher>[
-            isMethodCall('pickImage', arguments: <String, dynamic>{
-              'source': 0,
-              'maxWidth': null,
-              'maxHeight': null,
-              'imageQuality': null,
-              'cameraDevice': 0,
-            }),
-          ],
-        );
-      });
-
-      test('camera position can set to front', () async {
-        await picker.getImage(
-            source: ImageSource.camera,
-            preferredCameraDevice: CameraDevice.front);
-
-        expect(
-          log,
-          <Matcher>[
-            isMethodCall('pickImage', arguments: <String, dynamic>{
-              'source': 0,
-              'maxWidth': null,
-              'maxHeight': null,
-              'imageQuality': null,
-              'cameraDevice': 1,
-            }),
-          ],
-        );
+        test('retrieveLostData get both path and error should throw', () async {
+          channel.setMockMethodCallHandler((MethodCall methodCall) async {
+            return <String, String>{
+              'type': 'video',
+              'errorCode': 'test_error_code',
+              'errorMessage': 'test_error_message',
+              'path': '/example/path',
+            };
+          });
+          expect(picker.getLostData(), throwsAssertionError);
+        });
       });
     });
 
-    group('#pickVideo', () {
-      test('passes the image source argument correctly', () async {
-        await picker.getVideo(source: ImageSource.camera);
-        await picker.getVideo(source: ImageSource.gallery);
-
-        expect(
-          log,
-          <Matcher>[
-            isMethodCall('pickVideo', arguments: <String, dynamic>{
-              'source': 0,
-              'cameraDevice': 0,
-              'maxDuration': null,
-            }),
-            isMethodCall('pickVideo', arguments: <String, dynamic>{
-              'source': 1,
-              'cameraDevice': 0,
-              'maxDuration': null,
-            }),
-          ],
-        );
-      });
-
-      test('passes the duration argument correctly', () async {
-        await picker.getVideo(source: ImageSource.camera);
-        await picker.getVideo(
-            source: ImageSource.camera,
-            maxDuration: const Duration(seconds: 10));
-        await picker.getVideo(
-            source: ImageSource.camera,
-            maxDuration: const Duration(minutes: 1));
-        await picker.getVideo(
-            source: ImageSource.camera, maxDuration: const Duration(hours: 1));
-        expect(
-          log,
-          <Matcher>[
-            isMethodCall('pickVideo', arguments: <String, dynamic>{
-              'source': 0,
-              'maxDuration': null,
-              'cameraDevice': 0,
-            }),
-            isMethodCall('pickVideo', arguments: <String, dynamic>{
-              'source': 0,
-              'maxDuration': 10,
-              'cameraDevice': 0,
-            }),
-            isMethodCall('pickVideo', arguments: <String, dynamic>{
-              'source': 0,
-              'maxDuration': 60,
-              'cameraDevice': 0,
-            }),
-            isMethodCall('pickVideo', arguments: <String, dynamic>{
-              'source': 0,
-              'maxDuration': 3600,
-              'cameraDevice': 0,
-            }),
-          ],
-        );
-      });
-
-      test('handles a null video path response gracefully', () async {
-        channel.setMockMethodCallHandler((MethodCall methodCall) => null);
-
-        expect(await picker.getVideo(source: ImageSource.gallery), isNull);
-        expect(await picker.getVideo(source: ImageSource.camera), isNull);
-      });
-
-      test('camera position defaults to back', () async {
-        await picker.getVideo(source: ImageSource.camera);
-
-        expect(
-          log,
-          <Matcher>[
-            isMethodCall('pickVideo', arguments: <String, dynamic>{
-              'source': 0,
-              'cameraDevice': 0,
-              'maxDuration': null,
-            }),
-          ],
-        );
-      });
-
-      test('camera position can set to front', () async {
-        await picker.getVideo(
-            source: ImageSource.camera,
-            preferredCameraDevice: CameraDevice.front);
-
-        expect(
-          log,
-          <Matcher>[
-            isMethodCall('pickVideo', arguments: <String, dynamic>{
-              'source': 0,
-              'maxDuration': null,
-              'cameraDevice': 1,
-            }),
-          ],
-        );
-      });
-    });
-
-    group('#retrieveLostData', () {
-      test('retrieveLostData get success response', () async {
+    group('Multi images', () {
+      setUp(() {
         channel.setMockMethodCallHandler((MethodCall methodCall) async {
-          return <String, String>{
-            'type': 'image',
-            'path': '/example/path',
-          };
+          log.add(methodCall);
+          return [];
         });
-        final LostData response = await picker.getLostData();
-        expect(response.type, RetrieveType.image);
-        expect(response.file!.path, '/example/path');
+        log.clear();
       });
 
-      test('retrieveLostData get error response', () async {
-        channel.setMockMethodCallHandler((MethodCall methodCall) async {
-          return <String, String>{
-            'type': 'video',
-            'errorCode': 'test_error_code',
-            'errorMessage': 'test_error_message',
-          };
-        });
-        final LostData response = await picker.getLostData();
-        expect(response.type, RetrieveType.video);
-        expect(response.exception!.code, 'test_error_code');
-        expect(response.exception!.message, 'test_error_message');
-      });
+      group('#pickMultiImage', () {
+        test('passes the width and height arguments correctly', () async {
+          await picker.getMultiImage();
+          await picker.getMultiImage(
+            maxWidth: 10.0,
+          );
+          await picker.getMultiImage(
+            maxHeight: 10.0,
+          );
+          await picker.getMultiImage(
+            maxWidth: 10.0,
+            maxHeight: 20.0,
+          );
+          await picker.getMultiImage(
+            maxWidth: 10.0,
+            imageQuality: 70,
+          );
+          await picker.getMultiImage(
+            maxHeight: 10.0,
+            imageQuality: 70,
+          );
+          await picker.getMultiImage(
+              maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70);
 
-      test('retrieveLostData get null response', () async {
-        channel.setMockMethodCallHandler((MethodCall methodCall) async {
-          return null;
+          expect(
+            log,
+            <Matcher>[
+              isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+                'maxWidth': null,
+                'maxHeight': null,
+                'imageQuality': null,
+              }),
+              isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': null,
+                'imageQuality': null,
+              }),
+              isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+                'maxWidth': null,
+                'maxHeight': 10.0,
+                'imageQuality': null,
+              }),
+              isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': 20.0,
+                'imageQuality': null,
+              }),
+              isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': null,
+                'imageQuality': 70,
+              }),
+              isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+                'maxWidth': null,
+                'maxHeight': 10.0,
+                'imageQuality': 70,
+              }),
+              isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+                'maxWidth': 10.0,
+                'maxHeight': 20.0,
+                'imageQuality': 70,
+              }),
+            ],
+          );
         });
-        expect((await picker.getLostData()).isEmpty, true);
-      });
 
-      test('retrieveLostData get both path and error should throw', () async {
-        channel.setMockMethodCallHandler((MethodCall methodCall) async {
-          return <String, String>{
-            'type': 'video',
-            'errorCode': 'test_error_code',
-            'errorMessage': 'test_error_message',
-            'path': '/example/path',
-          };
+        test('does not accept a negative width or height argument', () {
+          expect(
+            picker.getMultiImage(maxWidth: -1.0),
+            throwsArgumentError,
+          );
+
+          expect(
+            picker.getMultiImage(maxHeight: -1.0),
+            throwsArgumentError,
+          );
         });
-        expect(picker.getLostData(), throwsAssertionError);
+
+        test('handles a null image path response gracefully', () async {
+          channel.setMockMethodCallHandler((MethodCall methodCall) => null);
+
+          expect(await picker.getMultiImage(), isNull);
+          expect(await picker.getMultiImage(), isNull);
+        });
       });
     });
   });