Skip to content

Commit 2a101a9

Browse files
committed
feat: improve error handling, avoid hardcoding string error codes, use pigeon for type-saftey
1 parent 53c3063 commit 2a101a9

File tree

12 files changed

+656
-201
lines changed

12 files changed

+656
-201
lines changed

packages/image_picker/image_picker_macos/example/integration_test/image_picker_test.dart

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
// found in the LICENSE file.
44

55
import 'package:example/main.dart';
6-
import 'package:flutter/services.dart';
76
import 'package:flutter_test/flutter_test.dart';
87
import 'package:image_picker_macos/image_picker_macos.dart';
9-
import 'package:image_picker_macos/src/messages.g.dart';
108
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
119
import 'package:integration_test/integration_test.dart';
1210

@@ -44,19 +42,5 @@ void main() {
4442
reason: 'Pressing the toggle button should update it correctly');
4543
},
4644
);
47-
testWidgets(
48-
'multi-video selection is not implemented',
49-
(WidgetTester tester) async {
50-
final ImagePickerApi hostApi = ImagePickerApi();
51-
await expectLater(
52-
hostApi.pickVideos(GeneralOptions(limit: 2)),
53-
throwsA(predicate<PlatformException>(
54-
(PlatformException e) =>
55-
e.code == 'UNIMPLEMENTED' &&
56-
e.message == 'Multi-video selection is not implemented',
57-
)),
58-
);
59-
},
60-
);
6145
});
6246
}

packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
archiveVersion = 1;
44
classes = {
55
};
6-
objectVersion = 60;
6+
objectVersion = 54;
77
objects = {
88

99
/* Begin PBXAggregateTarget section */

packages/image_picker/image_picker_macos/lib/image_picker_macos.dart

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:file_selector_macos/file_selector_macos.dart';
66
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
77
import 'package:flutter/foundation.dart';
8+
import 'package:flutter/services.dart';
89
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
910

1011
import 'src/messages.g.dart';
@@ -47,8 +48,6 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
4748
/// Supports picking an image, multi-image, video, media, and multiple media.
4849
bool useMacOSPHPicker = false;
4950

50-
// TODO(EchoEllet): avoid using @visibleForTesting per https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md#avoid-using-visiblefortesting
51-
5251
/// Return `true` if the current macOS version supports [useMacOSPHPicker].
5352
///
5453
/// The [PHPicker](https://developer.apple.com/documentation/photokit/phpickerviewcontroller)
@@ -144,6 +143,8 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
144143
_imageOptionsToImageSelectionOptions(options),
145144
GeneralOptions(limit: 1),
146145
))
146+
.getSuccessOrThrow()
147+
.filePaths
147148
.firstOrNull;
148149
if (imagePath == null) {
149150
return null;
@@ -183,7 +184,10 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
183184
case ImageSource.gallery:
184185
if (await shouldUsePHPicker()) {
185186
final String? videoPath =
186-
(await _hostApi.pickVideos(GeneralOptions(limit: 1))).firstOrNull;
187+
(await _hostApi.pickVideos(GeneralOptions(limit: 1)))
188+
.getSuccessOrThrow()
189+
.filePaths
190+
.firstOrNull;
187191
if (videoPath == null) {
188192
return null;
189193
}
@@ -228,12 +232,14 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
228232
MultiImagePickerOptions options = const MultiImagePickerOptions(),
229233
}) async {
230234
if (await shouldUsePHPicker()) {
231-
final List<String> images = await _hostApi.pickImages(
235+
final List<String> images = (await _hostApi.pickImages(
232236
_imageOptionsToImageSelectionOptions(options.imageOptions),
233237
GeneralOptions(
234238
limit: options.limit ?? 0,
235239
),
236-
);
240+
))
241+
.getSuccessOrThrow()
242+
.filePaths;
237243
return images.map((String imagePath) => XFile(imagePath)).toList();
238244
}
239245
const XTypeGroup typeGroup =
@@ -267,15 +273,17 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
267273
@override
268274
Future<List<XFile>> getMedia({required MediaOptions options}) async {
269275
if (await shouldUsePHPicker()) {
270-
final List<String> images = await _hostApi.pickMedia(
276+
final List<String> images = (await _hostApi.pickMedia(
271277
MediaSelectionOptions(
272278
imageSelectionOptions:
273279
_imageOptionsToImageSelectionOptions(options.imageOptions),
274280
),
275281
GeneralOptions(
276282
limit: options.limit ?? (options.allowMultiple ? 0 : 1),
277283
),
278-
);
284+
))
285+
.getSuccessOrThrow()
286+
.filePaths;
279287
return images.map((String mediaPath) => XFile(mediaPath)).toList();
280288
}
281289
const XTypeGroup typeGroup = XTypeGroup(
@@ -297,3 +305,47 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform {
297305
return files;
298306
}
299307
}
308+
309+
extension _ImagePickerResultExt on ImagePickerResult {
310+
/// Returns the result as an [ImagePickerSuccessResult], or throws a [PlatformException]
311+
/// if the result is an [ImagePickerErrorResult].
312+
ImagePickerSuccessResult getSuccessOrThrow() {
313+
final ImagePickerResult result = this;
314+
return switch (result) {
315+
ImagePickerSuccessResult() => result,
316+
ImagePickerErrorResult() => () {
317+
final String errorMessage = switch (result.error) {
318+
ImagePickerError.phpickerUnsupported =>
319+
'PHPicker is only supported on macOS 13.0 or newer.',
320+
ImagePickerError.windowNotFound =>
321+
'No active window to present the picker.',
322+
ImagePickerError.invalidImageSelection =>
323+
'One of the selected items is not an image.',
324+
ImagePickerError.invalidVideoSelection =>
325+
'One of the selected items is not a video.',
326+
ImagePickerError.imageLoadFailed =>
327+
'An error occurred while loading the image.',
328+
ImagePickerError.videoLoadFailed =>
329+
'An error occurred while loading the video.',
330+
ImagePickerError.imageConversionFailed =>
331+
'Failed to convert the NSImage to TIFF data.',
332+
ImagePickerError.imageSaveFailed =>
333+
'Error saving the NSImage data to a file.',
334+
ImagePickerError.imageCompressionFailed =>
335+
'Error while compressing the Data of the NSImage.',
336+
ImagePickerError.multiVideoSelectionUnsupported =>
337+
'The multi-video selection is not supported.',
338+
};
339+
// TODO(EchoEllet): Replace PlatformException with a plugin-specific exception.
340+
// This is currently implemented to maintain compatibility with the existing behavior
341+
// of other implementations of `image_picker`. For more details, refer to:
342+
// https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#platform-exception-handling
343+
throw PlatformException(
344+
code: result.error.name,
345+
message: errorMessage,
346+
details: result.platformErrorMessage,
347+
);
348+
}(),
349+
};
350+
}
351+
}

packages/image_picker/image_picker_macos/lib/src/messages.g.dart

Lines changed: 123 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2013 The Flutter Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4-
// Autogenerated from Pigeon (v22.6.1), do not edit directly.
4+
// Autogenerated from Pigeon (v22.7.2), do not edit directly.
55
// See also: https://pub.dev/packages/pigeon
66
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
77

@@ -29,6 +29,45 @@ List<Object?> wrapResponse(
2929
return <Object?>[error.code, error.message, error.details];
3030
}
3131

32+
/// Possible error conditions for [ImagePickerApi] calls.
33+
enum ImagePickerError {
34+
/// The current macOS version doesn't support [PHPickerViewController](https://developer.apple.com/documentation/photosui/phpickerviewcontroller)
35+
/// which is supported on macOS 13+.
36+
phpickerUnsupported,
37+
38+
/// Could not show the picker due to the missing window.
39+
windowNotFound,
40+
41+
/// When a `PHPickerResult` can't load `NSImage`. This error should not be reached
42+
/// as the filter in the `PHPickerConfiguration` is set to accept only images.
43+
invalidImageSelection,
44+
45+
/// When a `PHPickerResult` is not a video. This error should not be reached
46+
/// as the filter in the `PHPickerConfiguration` is set to accept only videos.
47+
invalidVideoSelection,
48+
49+
/// Could not load the image object as `NSImage`.
50+
imageLoadFailed,
51+
52+
/// Could not load the video data representation.
53+
videoLoadFailed,
54+
55+
/// The image tiff representation could not be loaded from the `NSImage`.
56+
imageConversionFailed,
57+
58+
/// The loaded `Data` from the `NSImage` could not be written as a file.
59+
imageSaveFailed,
60+
61+
/// The image could not be compressed or the `NSImage` could not be created
62+
/// from the compressed `Data`.
63+
imageCompressionFailed,
64+
65+
/// The multi-video selection is not supported as it's not supported in
66+
/// the app-facing package (`pickVideos` is missing).
67+
/// The multi-video selection is supported when using `pickMedia` instead.
68+
multiVideoSelectionUnsupported,
69+
}
70+
3271
/// The common options between [ImageSelectionOptions], [VideoSelectionOptions]
3372
/// and [MediaSelectionOptions].
3473
class GeneralOptions {
@@ -132,24 +171,85 @@ class MediaSelectionOptions {
132171
}
133172
}
134173

174+
sealed class ImagePickerResult {}
175+
176+
class ImagePickerSuccessResult extends ImagePickerResult {
177+
ImagePickerSuccessResult({
178+
required this.filePaths,
179+
});
180+
181+
/// The temporary file paths as a result of picking the images and/or videos.
182+
List<String> filePaths;
183+
184+
Object encode() {
185+
return <Object?>[
186+
filePaths,
187+
];
188+
}
189+
190+
static ImagePickerSuccessResult decode(Object result) {
191+
result as List<Object?>;
192+
return ImagePickerSuccessResult(
193+
filePaths: (result[0] as List<Object?>?)!.cast<String>(),
194+
);
195+
}
196+
}
197+
198+
class ImagePickerErrorResult extends ImagePickerResult {
199+
ImagePickerErrorResult({
200+
required this.error,
201+
this.platformErrorMessage,
202+
});
203+
204+
/// Potential error conditions for [ImagePickerApi] calls.
205+
ImagePickerError error;
206+
207+
/// Additional error message from the platform side.
208+
String? platformErrorMessage;
209+
210+
Object encode() {
211+
return <Object?>[
212+
error,
213+
platformErrorMessage,
214+
];
215+
}
216+
217+
static ImagePickerErrorResult decode(Object result) {
218+
result as List<Object?>;
219+
return ImagePickerErrorResult(
220+
error: result[0]! as ImagePickerError,
221+
platformErrorMessage: result[1] as String?,
222+
);
223+
}
224+
}
225+
135226
class _PigeonCodec extends StandardMessageCodec {
136227
const _PigeonCodec();
137228
@override
138229
void writeValue(WriteBuffer buffer, Object? value) {
139230
if (value is int) {
140231
buffer.putUint8(4);
141232
buffer.putInt64(value);
142-
} else if (value is GeneralOptions) {
233+
} else if (value is ImagePickerError) {
143234
buffer.putUint8(129);
235+
writeValue(buffer, value.index);
236+
} else if (value is GeneralOptions) {
237+
buffer.putUint8(130);
144238
writeValue(buffer, value.encode());
145239
} else if (value is MaxSize) {
146-
buffer.putUint8(130);
240+
buffer.putUint8(131);
147241
writeValue(buffer, value.encode());
148242
} else if (value is ImageSelectionOptions) {
149-
buffer.putUint8(131);
243+
buffer.putUint8(132);
150244
writeValue(buffer, value.encode());
151245
} else if (value is MediaSelectionOptions) {
152-
buffer.putUint8(132);
246+
buffer.putUint8(133);
247+
writeValue(buffer, value.encode());
248+
} else if (value is ImagePickerSuccessResult) {
249+
buffer.putUint8(134);
250+
writeValue(buffer, value.encode());
251+
} else if (value is ImagePickerErrorResult) {
252+
buffer.putUint8(135);
153253
writeValue(buffer, value.encode());
154254
} else {
155255
super.writeValue(buffer, value);
@@ -160,13 +260,20 @@ class _PigeonCodec extends StandardMessageCodec {
160260
Object? readValueOfType(int type, ReadBuffer buffer) {
161261
switch (type) {
162262
case 129:
163-
return GeneralOptions.decode(readValue(buffer)!);
263+
final int? value = readValue(buffer) as int?;
264+
return value == null ? null : ImagePickerError.values[value];
164265
case 130:
165-
return MaxSize.decode(readValue(buffer)!);
266+
return GeneralOptions.decode(readValue(buffer)!);
166267
case 131:
167-
return ImageSelectionOptions.decode(readValue(buffer)!);
268+
return MaxSize.decode(readValue(buffer)!);
168269
case 132:
270+
return ImageSelectionOptions.decode(readValue(buffer)!);
271+
case 133:
169272
return MediaSelectionOptions.decode(readValue(buffer)!);
273+
case 134:
274+
return ImagePickerSuccessResult.decode(readValue(buffer)!);
275+
case 135:
276+
return ImagePickerErrorResult.decode(readValue(buffer)!);
170277
default:
171278
return super.readValueOfType(type, buffer);
172279
}
@@ -188,6 +295,8 @@ class ImagePickerApi {
188295

189296
final String pigeonVar_messageChannelSuffix;
190297

298+
/// Returns whether [PHPickerViewController](https://developer.apple.com/documentation/photosui/phpickerviewcontroller)
299+
/// is supported on the current macOS version.
191300
Future<bool> supportsPHPicker() async {
192301
final String pigeonVar_channelName =
193302
'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.supportsPHPicker$pigeonVar_messageChannelSuffix';
@@ -217,7 +326,7 @@ class ImagePickerApi {
217326
}
218327
}
219328

220-
Future<List<String>> pickImages(
329+
Future<ImagePickerResult> pickImages(
221330
ImageSelectionOptions options, GeneralOptions generalOptions) async {
222331
final String pigeonVar_channelName =
223332
'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickImages$pigeonVar_messageChannelSuffix';
@@ -243,12 +352,12 @@ class ImagePickerApi {
243352
message: 'Host platform returned null value for non-null return value.',
244353
);
245354
} else {
246-
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<String>();
355+
return (pigeonVar_replyList[0] as ImagePickerResult?)!;
247356
}
248357
}
249358

250359
/// Currently, multi-video selection is unimplemented.
251-
Future<List<String>> pickVideos(GeneralOptions generalOptions) async {
360+
Future<ImagePickerResult> pickVideos(GeneralOptions generalOptions) async {
252361
final String pigeonVar_channelName =
253362
'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickVideos$pigeonVar_messageChannelSuffix';
254363
final BasicMessageChannel<Object?> pigeonVar_channel =
@@ -273,11 +382,11 @@ class ImagePickerApi {
273382
message: 'Host platform returned null value for non-null return value.',
274383
);
275384
} else {
276-
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<String>();
385+
return (pigeonVar_replyList[0] as ImagePickerResult?)!;
277386
}
278387
}
279388

280-
Future<List<String>> pickMedia(
389+
Future<ImagePickerResult> pickMedia(
281390
MediaSelectionOptions options, GeneralOptions generalOptions) async {
282391
final String pigeonVar_channelName =
283392
'dev.flutter.pigeon.image_picker_macos.ImagePickerApi.pickMedia$pigeonVar_messageChannelSuffix';
@@ -303,7 +412,7 @@ class ImagePickerApi {
303412
message: 'Host platform returned null value for non-null return value.',
304413
);
305414
} else {
306-
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<String>();
415+
return (pigeonVar_replyList[0] as ImagePickerResult?)!;
307416
}
308417
}
309418
}

0 commit comments

Comments
 (0)