Skip to content

Commit

Permalink
[image_picker_platform_interface] Introduce new PickedFile APIs. (flu…
Browse files Browse the repository at this point in the history
…tter#2791)

* Make API Async, so web can use objectUrls internally, instead of bytes.
* Introduce the PickedFile class to have a more platform agnostic return.
* Modify the platform interface to return PickedFiles.

Run tests with flutter test / flutter test --platform chrome

Co-authored-by: Rody Davis <rody.davis.jr@gmail.com>
  • Loading branch information
2 people authored and EdwinRomelta committed Jun 11, 2020
1 parent af61249 commit 82678b2
Show file tree
Hide file tree
Showing 17 changed files with 798 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.1.0

* Introduce PickedFile type for the new API.

## 1.0.1

* Update lower bound of dart dependency to 2.1.0.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
@visibleForTesting
MethodChannel get channel => _channel;

@override
Future<PickedFile> pickImage({
@required ImageSource source,
double maxWidth,
double maxHeight,
int imageQuality,
CameraDevice preferredCameraDevice = CameraDevice.rear,
}) async {
String path = await pickImagePath(
source: source,
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: imageQuality,
preferredCameraDevice: preferredCameraDevice,
);
return path != null ? PickedFile(path) : null;
}

@override
Future<String> pickImagePath({
@required ImageSource source,
Expand Down Expand Up @@ -53,6 +71,20 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
);
}

@override
Future<PickedFile> pickVideo({
@required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Duration maxDuration,
}) async {
String path = await pickVideoPath(
source: source,
maxDuration: maxDuration,
preferredCameraDevice: preferredCameraDevice,
);
return path != null ? PickedFile(path) : null;
}

@override
Future<String> pickVideoPath({
@required ImageSource source,
Expand All @@ -71,10 +103,48 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
}

@override
Future<LostData> retrieveLostData() async {
final Map<String, dynamic> result =
await _channel.invokeMapMethod<String, dynamic>('retrieve');

if (result == null) {
return LostData.empty();
}

assert(result.containsKey('path') ^ result.containsKey('errorCode'));

final String type = result['type'];
assert(type == kTypeImage || type == kTypeVideo);

RetrieveType retrieveType;
if (type == kTypeImage) {
retrieveType = RetrieveType.image;
} else if (type == kTypeVideo) {
retrieveType = RetrieveType.video;
}

PlatformException exception;
if (result.containsKey('errorCode')) {
exception = PlatformException(
code: result['errorCode'], message: result['errorMessage']);
}

final String path = result['path'];

return LostData(
file: path != null ? PickedFile(path) : null,
exception: exception,
type: retrieveType,
);
}

@override
// ignore: deprecated_member_use_from_same_package
Future<LostDataResponse> retrieveLostDataAsDartIoFile() async {
final Map<String, dynamic> result =
await _channel.invokeMapMethod<String, dynamic>('retrieve');
if (result == null) {
// ignore: deprecated_member_use_from_same_package
return LostDataResponse.empty();
}
assert(result.containsKey('path') ^ result.containsKey('errorCode'));
Expand All @@ -97,6 +167,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform {

final String path = result['path'];

// ignore: deprecated_member_use_from_same_package
return LostDataResponse(
file: path == null ? null : File(path),
exception: exception,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ abstract class ImagePickerPlatform extends PlatformInterface {
///
/// 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 [retrieveLostDataAsDartIoFile] when your app relaunches to retrieve the lost data.
@Deprecated('Use pickImage instead.')
Future<String> pickImagePath({
@required ImageSource source,
double maxWidth,
Expand All @@ -84,6 +85,7 @@ abstract class ImagePickerPlatform extends PlatformInterface {
///
/// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost
/// in this call. You can then call [retrieveLostDataAsDartIoFile] when your app relaunches to retrieve the lost data.
@Deprecated('Use pickVideo instead.')
Future<String> pickVideoPath({
@required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Expand All @@ -92,7 +94,7 @@ abstract class ImagePickerPlatform extends PlatformInterface {
throw UnimplementedError('pickVideoPath() has not been implemented.');
}

/// Retrieve the lost image file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only)
/// Retrieve the lost image file when [pickImagePath] or [pickVideoPath] failed because the MainActivity is destroyed. (Android only)
///
/// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive.
/// Call this method to retrieve the lost data and process the data according to your APP's business logic.
Expand All @@ -105,8 +107,81 @@ abstract class ImagePickerPlatform extends PlatformInterface {
/// See also:
/// * [LostDataResponse], for what's included in the response.
/// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction.
@Deprecated('Use retrieveLostData instead.')
Future<LostDataResponse> retrieveLostDataAsDartIoFile() {
throw UnimplementedError(
'retrieveLostDataAsDartIoFile() has not been implemented.');
}

// Next version of the API.

/// 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].
///
/// If specified, the image will be at most `maxWidth` wide and
/// `maxHeight` tall. Otherwise the image will be returned at it's
/// original width and height.
///
/// The `imageQuality` argument modifies the quality of the image, ranging from 0-100
/// where 100 is the original/max quality. If `imageQuality` is null, the image with
/// the original quality will be returned. Compression is only supportted for certain
/// image types such as JPEG. If compression is not supported for the image that is picked,
/// an warning message will be logged.
///
/// 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].
///
/// 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 [retrieveLostData] when your app relaunches to retrieve the lost data.
Future<PickedFile> pickImage({
@required ImageSource source,
double maxWidth,
double maxHeight,
int imageQuality,
CameraDevice preferredCameraDevice = CameraDevice.rear,
}) {
throw UnimplementedError('pickImage() has not been implemented.');
}

/// 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].
///
/// The [maxDuration] argument specifies the maximum duration of the captured video. If no [maxDuration] is specified,
/// the maximum duration will be infinite.
///
/// 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].
///
/// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost
/// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data.
Future<PickedFile> pickVideo({
@required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Duration maxDuration,
}) {
throw UnimplementedError('pickVideo() has not been implemented.');
}

/// Retrieve the lost [PickedFile] file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only)
///
/// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive.
/// Call this method to retrieve the lost data and process the data according to your APP's business logic.
///
/// Returns a [LostData] object if successfully retrieved the lost data. The [LostData] object can represent either a
/// successful image/video selection, or a failure.
///
/// Calling this on a non-Android platform will throw [UnimplementedError] exception.
///
/// See also:
/// * [LostData], for what's included in the response.
/// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction.
Future<LostData> retrieveLostData() {
throw UnimplementedError('retrieveLostData() has not been implemented.');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:image_picker_platform_interface/src/types/types.dart';
/// Only applies to Android.
/// See also:
/// * [ImagePicker.retrieveLostData] for more details on retrieving lost data.
@Deprecated('Use methods that return a LostData object instead.')
class LostDataResponse {
/// Creates an instance with the given [file], [exception], and [type]. Any of
/// the params may be null, but this is never considered to be empty.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:meta/meta.dart';

/// The interface for a PickedFile.
///
/// A PickedFile is a container that wraps the path of a selected
/// file by the user and (in some platforms, like web) the bytes
/// with the contents of the file.
///
/// This class is a very limited subset of dart:io [File], so all
/// the methods should seem familiar.
@immutable
abstract class PickedFileBase {
/// Construct a PickedFile
PickedFileBase(String path);

/// Get the path of the picked file.
///
/// This should only be used as a backwards-compatibility clutch
/// for mobile apps, or cosmetic reasons only (to show the user
/// the path they've picked).
///
/// Accessing the data contained in the picked file by its path
/// is platform-dependant (and won't work on web), so use the
/// byte getters in the PickedFile instance instead.
String get path {
throw UnimplementedError('.path has not been implemented.');
}

/// Synchronously read the entire file contents as a string using the given [Encoding].
///
/// By default, `encoding` is [utf8].
///
/// Throws Exception if the operation fails.
Future<String> readAsString({Encoding encoding = utf8}) {
throw UnimplementedError('readAsString() has not been implemented.');
}

/// Synchronously read the entire file contents as a list of bytes.
///
/// Throws Exception if the operation fails.
Future<Uint8List> readAsBytes() {
throw UnimplementedError('readAsBytes() has not been implemented.');
}

/// Create a new independent [Stream] for the contents of this file.
///
/// If `start` is present, the file will be read from byte-offset `start`. Otherwise from the beginning (index 0).
///
/// If `end` is present, only up to byte-index `end` will be read. Otherwise, until end of file.
///
/// In order to make sure that system resources are freed, the stream must be read to completion or the subscription on the stream must be cancelled.
Stream<Uint8List> openRead([int start, int end]) {
throw UnimplementedError('openRead() has not been implemented.');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:http/http.dart' as http show readBytes;

import './base.dart';

/// A PickedFile that works on web.
///
/// It wraps the bytes of a selected file.
class PickedFile extends PickedFileBase {
final String path;
final Uint8List _initBytes;

/// Construct a PickedFile object from its ObjectUrl.
///
/// Optionally, this can be initialized with `bytes`
/// so no http requests are performed to retrieve files later.
PickedFile(this.path, {Uint8List bytes})
: _initBytes = bytes,
super(path);

Future<Uint8List> get _bytes async {
if (_initBytes != null) {
return Future.value(UnmodifiableUint8ListView(_initBytes));
}
return http.readBytes(path);
}

@override
Future<String> readAsString({Encoding encoding = utf8}) async {
return encoding.decode(await _bytes);
}

@override
Future<Uint8List> readAsBytes() async {
return Future.value(await _bytes);
}

@override
Stream<Uint8List> openRead([int start, int end]) async* {
final bytes = await _bytes;
yield bytes.sublist(start ?? 0, end ?? bytes.length);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import './base.dart';

/// A PickedFile backed by a dart:io File.
class PickedFile extends PickedFileBase {
final File _file;

/// Construct a PickedFile object backed by a dart:io File.
PickedFile(String path)
: _file = File(path),
super(path);

@override
String get path {
return _file.path;
}

@override
Future<String> readAsString({Encoding encoding = utf8}) {
return _file.readAsString(encoding: encoding);
}

@override
Future<Uint8List> readAsBytes() {
return _file.readAsBytes();
}

@override
Stream<Uint8List> openRead([int start, int end]) {
return _file
.openRead(start ?? 0, end)
.map((chunk) => Uint8List.fromList(chunk));
}
}
Loading

0 comments on commit 82678b2

Please sign in to comment.