Flutter image_picker 插件实战指南:平台配置、相机委托与 1.0 API 迁移
【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages
本文以 Flutter 官方维护的image_picker插件(当前仓库内版本 1.2.3)为主线,系统讲解如何从系统相册挑选图片/视频、调用相机拍摄、处理 Android 低内存下 Activity 被销毁导致的数据丢失,以及桌面端通过cameraDelegate扩展相机能力,并完整覆盖从旧版PickedFileAPI 迁移到XFileAPI 的改造要点。读完本文,你将能够在一行代码内完成图片/视频的选取与拍摄,并为 iOS、Android 与桌面平台正确配置权限与运行环境。
插件概览与平台支持
image_picker是一个 Flutter 插件,用于"从系统相册挑选图片/视频,或使用相机拍摄新照片"。其主入口类为ImagePicker,所有能力都通过它暴露给业务代码。
从 pubspec.yaml 的插件声明可以看出,它是一个"联邦式(federated)"插件:主包本身不实现任何平台逻辑,而是按平台委托给独立实现包:
| 平台 | 默认实现包 |
|---|---|
| Android | image_picker_android |
| iOS | image_picker_ios |
| Linux | image_picker_linux |
| macOS | image_picker_macos |
| Web | image_picker_for_web |
| Windows | image_picker_windows |
各平台的最低系统要求如下:
| 平台 | Android | iOS | Linux | macOS | Web | Windows |
|---|---|---|---|---|---|---|
| 支持 | SDK 24+ | iOS 13+ | 任意 | 10.15+ | 详见 image_picker_for_web 的 Web 平台限制 | Windows 10+ |
底层架构上,image_picker依赖image_picker_platform_interface中定义的ImagePickerPlatform抽象类。该类默认持有MethodChannelImagePicker实例,平台实现通过ImagePickerPlatform.instance注册自己。这也是桌面端相机委托机制(下文会讲)能够生效的基础。
平台环境配置
iOS 配置:Info.plist 权限声明
自0.8.1版本起,iOS 实现改用 PHPicker 在 iOS 14+ 上选取(多张)图片。随之而来的一个已知问题是:在 iOS 14+ 的模拟器上无法选取 HEIC 图片。建议在真机上测试,或改用非 HEIC 图片进行测试,直至 Apple 修复该问题。
需要在<project root>/ios/Runner/Info.plist中添加以下键:
NSPhotoLibraryUsageDescription—— 说明应用为何需要访问相册的权限,在 Xcode 可视化编辑器中显示为Privacy - Photo Library Usage Description。- 如果你始终为
requestFullMetadata传false,系统不会请求该权限;但 App Store 审核政策要求 plist 中仍须包含此条目。
- 如果你始终为
NSCameraUsageDescription—— 说明应用为何需要访问相机,显示为Privacy - Camera Usage Description。NSMicrophoneUsageDescription—— 如果你打算录制视频,需说明麦克风访问用途,显示为Privacy - Microphone Usage Description。
Android 配置
Android 端无需任何额外配置即可开箱即用。历史上需要在AndroidManifest.xml的<application>标签上添加android:requestLegacyExternalStorage="true",如今image_picker已迁移到 scoped storage(分区存储),该属性不再需要。
但强烈建议为以下两个场景做防御性处理:
处理 MainActivity 被系统销毁(Handling MainActivity destruction)
当系统内存压力较大时,Android 可能销毁正在使用 image_picker 的应用的 MainActivity。Android 端实现依赖默认的Intent.ACTION_GET_CONTENT或MediaStore.ACTION_IMAGE_CAPTUREintent:intent 执行期间,源应用被移到后台并成为低内存清理的候选对象;intent 结束后 Android 会重启应用,但选取结果永远不会返回给原始调用方。
此时应调用ImagePicker.retrieveLostData()找回丢失的数据。官方建议在每次应用启动时都执行该检查(该场景同样适用于pickMultiImage、pickVideo、pickMedia,详见 image_picker.dart 的注释):
Future<void> getLostData() async { final picker = ImagePicker(); final LostDataResponse response = await picker.retrieveLostData(); if (response.isEmpty) { return; } final List<XFile>? files = response.files; if (files != null) { _handleLostFiles(files); } else { _handleError(response.exception); } }从 lost_data_response.dart 的源码可以看到LostDataResponse的结构:它可能携带成功选取的file/files,也可能携带上次选取抛出的exception(注意:该异常是选取过程本身的异常,而非导致 MainActivity 被销毁的异常),type则区分是 image、video 还是 media。isEmpty为true表示没有丢失的数据。注意retrieveLostData()仅适用于 Android,在非 Android 平台调用会抛出UnimplementedError。完整流程可参考 example 应用。
永久保存图片和视频
用相机拍摄的图片/视频默认保存在应用本地缓存目录中,随时可能被系统清理,只能视为临时文件。如果需要永久保存,必须由开发者自行将其移动到更持久的位置(如应用文档目录或用户指定的存储位置)。
Android 13+ 的 Photo Picker
在 Android 13 及以上系统,本插件使用 Android 官方的 Android Photo Picker;在 Android 12 及以下,使用 Photo Picker 是可选的。具体接入方式见image_picker_android。
避免使用launchMode: singleInstance
从设置了launchMode: singleInstance的 Activity 启动图片选择器,将总是返回RESULT_CANCELED。在该启动模式下,新 Activity 会被创建在独立的 Task 中,而不同 Task 之间的 Activity 无法通信,因此图片选择器无法把最终结果回传给调用方 Activity。解决办法是改用launchMode: singleTask。
Windows、macOS 与 Linux:桌面端有限支持与相机委托
桌面端目前是有限支持:实现本质上是file_selector插件的一层包装,并预设了合适的文件类型过滤器。因此选择相关的修改选项(如最大宽高)尚不支持。
默认情况下桌面端不支持ImageSource.camera(Android/iOS 之外没有系统级的拍照 UI)。但桌面实现允许通过设置cameraDelegate把相机调用委托给自定义实现,例如在main()中:
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; // ··· class MyCameraDelegate extends ImagePickerCameraDelegate { @override Future<XFile?> takePhoto({ ImagePickerCameraDelegateOptions options = const ImagePickerCameraDelegateOptions(), }) async { return _takeAPhoto(options.preferredCameraDevice); } @override Future<XFile?> takeVideo({ ImagePickerCameraDelegateOptions options = const ImagePickerCameraDelegateOptions(), }) async { return _takeAVideo(options.preferredCameraDevice); } } // ··· void setUpCameraDelegate() { final ImagePickerPlatform instance = ImagePickerPlatform.instance; if (instance is CameraDelegatingImagePickerPlatform) { instance.cameraDelegate = MyCameraDelegate(); } }设置cameraDelegate之后,使用ImageSource.camera的image_picker调用就会正常地转发给你提供的委托。官方鼓励社区构建实现ImagePickerCameraDelegate的包,为桌面相机 UI 提供更多方案。
从 image_picker_platform.dart 的源码可以看到委托机制的实现细节:
CameraDelegatingImagePickerPlatform是ImagePickerPlatform的子类,持有可空的cameraDelegate;- 当
cameraDelegate为null时,调用ImageSource.camera会抛出StateError,并提示"需要 cameraDelegate"; - 委托的
takePhoto/takeVideo方法接收ImagePickerCameraDelegateOptions,其中包含preferredCameraDevice(默认后置摄像头)与maxVideoDuration(录视频最大时长,默认无限制),见 camera_delegate.dart; - 同时
supportsImageSource(ImageSource.camera)在未设置 delegate 时返回false——业务代码可先用该方法探测当前平台是否支持相机,避免运行时抛错。
macOS 额外配置
由于 macOS 实现基于file_selector,需要为应用添加文件系统访问 entitlement(在 macOS 的 entitlements 文件中):
<key>com.apple.security.files.user-selected.read-only</key> <true/>核心 API 用法
快速上手示例
以下代码覆盖了单图/多图、单视频/多视频、图片与视频混合选取的全部入口,与 readme_excerpts.dart 中的示例一致:
final picker = ImagePicker(); // 从相册选一张图片。 final XFile? image = await picker.pickImage(source: ImageSource.gallery); // 用相机拍一张照片。 final XFile? photo = await picker.pickImage(source: ImageSource.camera); // 从相册选一个视频。 final XFile? galleryVideo = await picker.pickVideo(source: ImageSource.gallery); // 用相机录一段视频。 final XFile? cameraVideo = await picker.pickVideo(source: ImageSource.camera); // 一次选多张图片。 final List<XFile> images = await picker.pickMultiImage(); // 选单个图片或视频。 final XFile? media = await picker.pickMedia(); // 选多个图片和视频。 final List<XFile> medias = await picker.pickMultipleMedia();所有返回的XFile均来自 cross_file 包,统一了跨平台文件抽象。注意:返回的XFile只在单次应用会话内有效,不要跨会话保存其文件路径(参见 image_picker.dart 中每个方法的注释)。
ImageSource与完整参数详解
ImageSource枚举只有两个取值,定义见 image_source.dart:
camera—— 打开设备相机,让用户拍一张新照片/录一段新视频;gallery—— 打开用户相册。
pickImage参数
Future<XFile?> pickImage({ required ImageSource source, double? maxWidth, double? maxHeight, int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, bool requestFullMetadata = true, })| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
source | ImageSource | 必填 | 图片来源:相机或相册 |
maxWidth | double? | null | 图片最大宽度(像素)。指定后图片会被等比缩放到不超过该宽度;与maxHeight同时为空时保持原尺寸 |
maxHeight | double? | null | 图片最大高度(像素),规则同上 |
imageQuality | int? | null | 压缩质量,0–100,100 为原始质量。null表示返回原始质量 |
preferredCameraDevice | CameraDevice | CameraDevice.rear | 相机来源为camera时优先使用的前/后置摄像头;相册来源时被忽略;设备不支持时也被忽略。注意 Android 的 intent 没有公开参数指定前后摄像头,因此该功能在 Android 上不保证生效 |
requestFullMetadata | bool | true | 是否请求完整图片元数据。设为true时可能触发额外权限请求(如 iOS 的相册权限);始终传false则 iOS 不会请求NSPhotoLibraryUsageDescription权限 |
参数校验规则(来自 image_options.dart 的_validateOptions):
imageQuality必须在 0–100 之间,否则抛ArgumentError;maxWidth/maxHeight不能为负数,否则抛ArgumentError。
HEIC 兼容性:iOS 支持 HEIC 图片,而 Android 8 及以下不支持;Android 9 及以上只有在配合尺寸修改(指定maxWidth/maxHeight)时才支持 HEIC。压缩(imageQuality)仅对部分格式有效——如 JPEG,以及 Android 平台上的 PNG 和 WebP;若选取的图片格式不支持压缩,插件会打印警告日志。
pickVideo参数
Future<XFile?> pickVideo({ required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, })source—— 必填,同pickImage;preferredCameraDevice—— 默认CameraDevice.rear,规则同pickImage;maxDuration—— 仅在相机拍摄(ImageSource.camera)时生效,限制视频最大时长;为null时不限时长;从相册选取视频时该参数被忽略。
pickMultiImage/pickMultipleMedia/pickMultiVideo
三者均支持limit参数限定最大可选数量,且各平台可能忽略该值。pickMultiImage与pickMultipleMedia在limit小于 1 时抛ArgumentError;当limit == 1时,源码会内部转调单选的pickImage/pickMedia并包装成单元素列表(见 image_picker.dart 与pickMultipleMedia的实现)。pickMultiVideo还支持maxDuration限制视频时长。另外,pickMultiImage、pickMedia、pickMultipleMedia、pickMultiVideo均不支持 iOS 14 以下版本。
异常情况
pick*系列方法可能抛出PlatformException,常见触发场景包括:应用没有相机/相册权限、设备无可用相机、插件已被占用(上一次选取尚未结束)、iOS 端临时文件创建失败、Android 端插件 Activity 分配失败,以及未知错误。业务代码应对这些异常做兜底处理。
迁移到 1.0:从PickedFile到XFile
自0.8.2版本起,image_picker 新增了返回XFile(来自 cross_file 包)的方法,替代插件自有的PickedFile。旧方法在 0.8.9 之前一直保持兼容,并在 1.0.0 中被移除。
在 image_picker_platform.dart 中可以看到,旧的pickImage、pickMultiImage、pickVideo、retrieveLostData等方法均被标记为@Deprecated,并抛出UnimplementedError,即当前版本中这些接口已经不再可用。
迁移对照表:
| 旧 API(已移除) | 新 API(当前使用) |
|---|---|
PickedFile image = await _picker.getImage(...) | XFile image = await _picker.pickImage(...) |
List<PickedFile> images = await _picker.getMultiImage(...) | List<XFile> images = await _picker.pickMultiImage(...) |
PickedFile video = await _picker.getVideo(...) | XFile video = await _picker.pickVideo(...) |
LostData response = await _picker.getLostData() | LostDataResponse response = await _picker.retrieveLostData() |
升级到 1.x 时,只需将返回值类型从PickedFile替换为XFile、方法名从get*替换为pick*,并注意LostDataResponse相比旧LostData增加了files(多选时丢失的文件列表)与type(image/video/media 类型)等字段,isEmpty表示无丢失数据。同时,1.0 之后新增了pickMedia、pickMultipleMedia、pickMultiVideo等混合媒体选取能力,以及桌面端cameraDelegate扩展点,均可在 ImagePicker 类中直接使用。
小结
image_picker以一行pick*调用覆盖了相册选取、相机拍摄、多选与混合媒体等主流场景,是 Flutter 官方插件体系中媒体入口的标准选择。落地时需重点关注三点:iOS 的权限 plist 声明(NSPhotoLibraryUsageDescription/NSCameraUsageDescription/NSMicrophoneUsageDescription)、Android 低内存下 MainActivity 被销毁时的retrieveLostData()启动检查,以及桌面端通过cameraDelegate补齐相机能力。相关可运行示例见 example 应用,各平台实现与平台接口可分别在packages/image_picker/目录下的对应子包中查阅。
【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考