Skip to content

[image_picker] Add desktop support - platform interface #4161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 2.7.0

* Adds `CameraDelegatingImagePickerPlatform` as a base class for platform
implementations that don't support `ImageSource.camera`, but allow for an-
implementation to be provided at the application level via implementation
of `CameraDelegatingImagePickerPlatform`.
* Adds `supportsImageSource` to check source support at runtime.

## 2.6.4

* Adds compatibility with `http` 1.0.
Expand Down Expand Up @@ -32,7 +40,7 @@
* Adds `requestFullMetadata` option that allows disabling extra permission requests
on certain platforms.
* Moves optional image picking parameters to `ImagePickerOptions` class.
* Minor fixes for new analysis options.
* Minor fixes for new analysis options.

## 2.4.4

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ abstract class ImagePickerPlatform extends PlatformInterface {

/// Platform-specific plugins should set this with their own platform-specific
/// class that extends [ImagePickerPlatform] when they register themselves.
// TODO(amirh): Extract common platform interface logic.
// https://github.com/flutter/flutter/issues/43368
static set instance(ImagePickerPlatform instance) {
PlatformInterface.verify(instance, _token);
_instance = instance;
Expand Down Expand Up @@ -305,4 +303,75 @@ abstract class ImagePickerPlatform extends PlatformInterface {
);
return pickedImages ?? <XFile>[];
}

/// Returns true if the implementation supports [source].
///
/// Defaults to true for the original image sources, `gallery` and `camera`,
/// for backwards compatibility.
bool supportsImageSource(ImageSource source) {
return source == ImageSource.gallery || source == ImageSource.camera;
}
}

/// A base class for an [ImagePickerPlatform] implementation that does not
/// directly support [ImageSource.camera], but supports delegating to a
/// provided [ImagePickerCameraDelegate].
abstract class CameraDelegatingImagePickerPlatform extends ImagePickerPlatform {
/// A delegate to respond to calls that use [ImageSource.camera].
///
/// When it is null, attempting to use [ImageSource.camera] will throw a
/// [StateError].
ImagePickerCameraDelegate? cameraDelegate;

@override
bool supportsImageSource(ImageSource source) {
if (source == ImageSource.camera) {
return cameraDelegate != null;
}
return super.supportsImageSource(source);
}

@override
Future<XFile?> getImageFromSource({
required ImageSource source,
ImagePickerOptions options = const ImagePickerOptions(),
}) async {
if (source == ImageSource.camera) {
final ImagePickerCameraDelegate? delegate = cameraDelegate;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Dart won't promote nullable fields (because in general it can't guarantee that you aren't running this code in the context of a subclass that overrode the field with a getter that does something crazy like return null every other time you call it), you have two options for cases like this:

  • Make a local variable, null check it to promote it, and then use it as non-nullable.
  • Null-check the field, then use ! every time you access it.

I strongly prefer the former since it relies on the compiler to ensure that things are still right any time someone changes the logic, while the latter relies on humans to ensure it.

if (delegate == null) {
throw StateError(
'This implementation of ImagePickerPlatform requires a '
'"cameraDelegate" in order to use ImageSource.camera');
}
return delegate.takePhoto(
options: ImagePickerCameraDelegateOptions(
preferredCameraDevice: options.preferredCameraDevice,
));
}
return super.getImageFromSource(source: source, options: options);
}

@override
Future<XFile?> getVideo({
required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Duration? maxDuration,
}) async {
if (source == ImageSource.camera) {
final ImagePickerCameraDelegate? delegate = cameraDelegate;
if (delegate == null) {
throw StateError(
'This implementation of ImagePickerPlatform requires a '
'"cameraDelegate" in order to use ImageSource.camera');
}
return delegate.takeVideo(
options: ImagePickerCameraDelegateOptions(
preferredCameraDevice: preferredCameraDevice,
maxVideoDuration: maxDuration));
}
return super.getVideo(
source: source,
preferredCameraDevice: preferredCameraDevice,
maxDuration: maxDuration);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:cross_file/cross_file.dart';
import 'package:flutter/foundation.dart' show immutable;

import 'camera_device.dart';

/// Options for [ImagePickerCameraDelegate] methods.
///
/// New options may be added in the future.
@immutable
class ImagePickerCameraDelegateOptions {
/// Creates a new set of options for taking an image or video.
const ImagePickerCameraDelegateOptions({
this.preferredCameraDevice = CameraDevice.rear,
this.maxVideoDuration,
});

/// The camera device to default to, if available.
///
/// Defaults to [CameraDevice.rear].
final CameraDevice preferredCameraDevice;

/// The maximum duration to allow when recording a video.
///
/// Defaults to null, meaning no maximum duration.
final Duration? maxVideoDuration;
}

/// A delegate for `ImagePickerPlatform` implementations that do not provide
/// a camera implementation, or that have a default but allow substituting an
/// alternate implementation.
abstract class ImagePickerCameraDelegate {
/// Takes a photo with the given [options] and returns an [XFile] to the
/// resulting image file.
///
/// Returns null if the photo could not be taken, or the user cancelled.
Future<XFile?> takePhoto({
ImagePickerCameraDelegateOptions options =
const ImagePickerCameraDelegateOptions(),
});

/// Records a video with the given [options] and returns an [XFile] to the
/// resulting video file.
///
/// Returns null if the video could not be recorded, or the user cancelled.
Future<XFile?> takeVideo({
ImagePickerCameraDelegateOptions options =
const ImagePickerCameraDelegateOptions(),
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

export 'camera_delegate.dart';
export 'camera_device.dart';
export 'image_options.dart';
export 'image_picker_options.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/image_picker/
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
version: 2.6.4
version: 2.7.0

environment:
sdk: ">=2.18.0 <4.0.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter_test/flutter_test.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';

void main() {
group('ImagePickerPlatform', () {
test('supportsImageSource defaults to true for original values', () async {
final ImagePickerPlatform implementation = FakeImagePickerPlatform();

expect(implementation.supportsImageSource(ImageSource.camera), true);
expect(implementation.supportsImageSource(ImageSource.gallery), true);
});
});

group('CameraDelegatingImagePickerPlatform', () {
test(
'supportsImageSource returns false for camera when there is no delegate',
() async {
final FakeCameraDelegatingImagePickerPlatform implementation =
FakeCameraDelegatingImagePickerPlatform();

expect(implementation.supportsImageSource(ImageSource.camera), false);
});

test('supportsImageSource returns true for camera when there is a delegate',
() async {
final FakeCameraDelegatingImagePickerPlatform implementation =
FakeCameraDelegatingImagePickerPlatform();
implementation.cameraDelegate = FakeCameraDelegate();

expect(implementation.supportsImageSource(ImageSource.camera), true);
});

test('getImageFromSource for camera throws if delegate is not set',
() async {
final FakeCameraDelegatingImagePickerPlatform implementation =
FakeCameraDelegatingImagePickerPlatform();

await expectLater(
implementation.getImageFromSource(source: ImageSource.camera),
throwsStateError);
});

test('getVideo for camera throws if delegate is not set', () async {
final FakeCameraDelegatingImagePickerPlatform implementation =
FakeCameraDelegatingImagePickerPlatform();

await expectLater(implementation.getVideo(source: ImageSource.camera),
throwsStateError);
});

test('getImageFromSource for camera calls delegate if set', () async {
const String fakePath = '/tmp/foo';
final FakeCameraDelegatingImagePickerPlatform implementation =
FakeCameraDelegatingImagePickerPlatform();
implementation.cameraDelegate =
FakeCameraDelegate(result: XFile(fakePath));

expect(
(await implementation.getImageFromSource(source: ImageSource.camera))!
.path,
fakePath);
});

test('getVideo for camera calls delegate if set', () async {
const String fakePath = '/tmp/foo';
final FakeCameraDelegatingImagePickerPlatform implementation =
FakeCameraDelegatingImagePickerPlatform();
implementation.cameraDelegate =
FakeCameraDelegate(result: XFile(fakePath));

expect((await implementation.getVideo(source: ImageSource.camera))!.path,
fakePath);
});
});
}

class FakeImagePickerPlatform extends ImagePickerPlatform {}

class FakeCameraDelegatingImagePickerPlatform
extends CameraDelegatingImagePickerPlatform {}

class FakeCameraDelegate extends ImagePickerCameraDelegate {
FakeCameraDelegate({this.result});

XFile? result;

@override
Future<XFile?> takePhoto(
{ImagePickerCameraDelegateOptions options =
const ImagePickerCameraDelegateOptions()}) async {
return result;
}

@override
Future<XFile?> takeVideo(
{ImagePickerCameraDelegateOptions options =
const ImagePickerCameraDelegateOptions()}) async {
return result;
}
}