📸 Flutter image_picker 鸿蒙化适配实战教程

你好!👋 欢迎来到这篇关于 image_picker 在 HarmonyOS (OpenHarmony) 上使用的实战教程。当前已经完成鸿蒙化适配的Flutter三方库均可在flutter_packages仓库下查看。

本文将手把手带你完成图片选择、相机拍照、多图选择等功能的实现,并教你如何进行二次封装提升代码复用性!📱


📦 1. 引入依赖:pubspec.yaml

首先,我们需要在项目的配置文件中引入适配后的 image_picker 库。

修改文件pubspec.yaml

操作:在 dependencies 节点下添加 image_picker 的 git 依赖配置。

dependencies:
  flutter:
    sdk: flutter
  
  # 👇 新增:引入 image_picker 鸿蒙适配版本
  image_picker:
    git: 
      url: https://gitcode.com/openharmony-tpc/flutter_packages.git
      path: packages/image_picker/image_picker
      ref: br_image_picker-v1.1.2_ohos

💡 提示:修改完成后,别忘了运行终端命令 flutter pub get 来下载依赖!

🎯 为什么选择 Git 方式引入?

file_selector 类似,鸿蒙适配版本的 image_picker 目前由社区维护,尚未发布到 pub.dev。通过 Git 方式引入,我们可以:

  • 🚀 第一时间体验最新功能
  • 🔧 获取社区的及时修复
  • 🎯 确保与鸿蒙平台完美兼容

👶 新手小课堂:image_picker 是什么?

image_picker 是 Flutter 官方提供的一个插件,用于从设备相册选择图片/视频或使用相机拍摄照片/录制视频

想象一下,你的应用需要让用户上传头像或分享照片。传统方式需要分别编写 Android、iOS、鸿蒙等平台的原生代码。而 image_picker 就像一个 🎮 统一遥控器,你只需要按一个按钮(调用一个方法),它就会自动调用各个平台的原生相册或相机功能!

核心优势:

  • 跨平台统一:一套代码适配多个平台
  • 功能强大:支持相机、相册、单选、多选、视频
  • 简单易用:API 设计简洁,上手快速
  • 质量控制:支持图片质量压缩、尺寸限制

🛠️ 2. 二次封装:lib/services/image_picker_service.dart

为了提高代码复用性和可维护性,我们创建一个服务类对 image_picker 进行二次封装。

2.1 为什么需要二次封装?

问题场景:

  • ❌ 直接使用 ImagePicker 会导致代码重复
  • ❌ 每次都要写 try-catch 异常处理
  • ❌ 参数配置分散在各处,不便于统一管理
  • ❌ 业务逻辑和 UI 代码耦合

二次封装的好处:

  • 代码复用:封装常用功能,避免重复编写
  • 统一管理:集中处理错误、日志、默认参数
  • 易于维护:修改只需在一处进行
  • 业务分离:UI 层只关注展示,逻辑层负责数据获取

2.2 创建服务类

新建文件lib/services/image_picker_service.dart

📊 Image Picker 服务架构流程:

相机

相册单选

相册多选

视频

🎨 UI层调用

📦 ImagePickerService单例

🎯 选择来源

📷 pickImageFromCamera

🖼️ pickImageFromGallery

📚 pickMultipleImages

🎥 pickVideo

⚙️ ImagePicker原生调用

🔗 Platform Channel

🎯 鸿蒙系统API

📸 系统相机/相册

📄 返回XFile结果

📖 readAsBytes读取文件流

✨ Image.memory展示

核心代码实现:

import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/foundation.dart';

/// 📸 图片选择器服务类
/// 对 image_picker 进行二次封装,提供统一的图片选择接口
class ImagePickerService {
  // 单例模式
  static final ImagePickerService _instance = ImagePickerService._internal();
  factory ImagePickerService() => _instance;
  ImagePickerService._internal();

  final ImagePicker _picker = ImagePicker();

  /// 📷 从相机拍照
  Future<XFile?> pickImageFromCamera({
    int imageQuality = 80,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
  }) async {
    try {
      final XFile? image = await _picker.pickImage(
        source: ImageSource.camera,
        imageQuality: imageQuality,
        preferredCameraDevice: preferredCameraDevice,
      );
      return image;
    } catch (e) {
      if (kDebugMode) {
        print('📷 相机拍照错误: $e');
      }
      rethrow;
    }
  }

  /// 🖼️ 从相册选择单张图片
  Future<XFile?> pickImageFromGallery({
    int imageQuality = 80,
    double? maxWidth,
    double? maxHeight,
  }) async {
    try {
      final XFile? image = await _picker.pickImage(
        source: ImageSource.gallery,
        imageQuality: imageQuality,
        maxWidth: maxWidth,
        maxHeight: maxHeight,
      );
      return image;
    } catch (e) {
      if (kDebugMode) {
        print('🖼️ 相册选择错误: $e');
      }
      rethrow;
    }
  }

  /// 📚 从相册选择多张图片
  Future<List<XFile>> pickMultipleImages({
    int imageQuality = 80,
    double? maxWidth,
    double? maxHeight,
  }) async {
    try {
      final List<XFile> images = await _picker.pickMultiImage(
        imageQuality: imageQuality,
        maxWidth: maxWidth,
        maxHeight: maxHeight,
      );
      return images;
    } catch (e) {
      if (kDebugMode) {
        print('📚 多图选择错误: $e');
      }
      return [];
    }
  }

  /// 📊 获取图片文件信息
  Future<Map<String, dynamic>> getImageInfo(XFile image) async {
    try {
      final bytes = await image.readAsBytes();
      return {
        'name': image.name,
        'path': image.path,
        'size': bytes.length,
        'sizeInKB': (bytes.length / 1024).toStringAsFixed(2),
        'sizeInMB': (bytes.length / (1024 * 1024)).toStringAsFixed(2),
        'mimeType': image.mimeType,
      };
    } catch (e) {
      if (kDebugMode) {
        print('📊 获取图片信息错误: $e');
      }
      return {};
    }
  }
}

👶 新手小课堂:单例模式的作用

你可能注意到了这段代码:

static final ImagePickerService _instance = ImagePickerService._internal();
factory ImagePickerService() => _instance;
ImagePickerService._internal();

这是 单例模式(Singleton Pattern) 的实现。

为什么需要单例?

想象一下,如果每次需要选择图片时都创建一个新的 ImagePickerService 对象,就像每次出门都买一辆新车 🚗。这样不仅浪费资源,还可能导致状态不一致。

使用单例模式后,整个应用只会创建一个 ImagePickerService 实例,就像全家共用一辆车 🚙,既节省资源,又便于管理!

✨ 单例模式的优势:

  • 💾 节省内存:避免重复创建对象
  • 🔒 状态一致:全局共享同一实例
  • 性能优化:减少对象创建开销
  • 🎯 统一管理:集中配置和控制

💻 3. 使用方法:lib/image_picker_demo.dart

接下来,我们创建一个完整的演示页面,展示如何使用封装好的服务类。

新建文件lib/image_picker_demo.dart

3.1 页面功能概览

我们的演示页面将实现以下功能:

  • 📷 相机拍照:调用系统相机拍摄照片
  • 🖼️ 相册单选:从相册选择一张图片
  • 📚 相册多选:从相册选择多张图片
  • 📊 图片信息:显示图片的详细信息
  • 🗑️ 清除图片:清空已选择的图片

3.2 核心代码实现

导入依赖:

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:typed_data';
import 'services/image_picker_service.dart';

实现相机拍照:

final ImagePickerService _imagePickerService = ImagePickerService();
XFile? _cameraImage;

Future<void> _pickFromCamera() async {
  setState(() => _isLoading = true);
  try {
    final XFile? image = await _imagePickerService.pickImageFromCamera(
      imageQuality: 85, // 图片质量 0-100
    );
    if (image != null) {
      setState(() {
        _cameraImage = image;
      });
      _showSuccessSnackBar('📷 相机拍照成功!');
    }
  } catch (e) {
    _showErrorSnackBar('相机拍照失败: $e');
  } finally {
    setState(() => _isLoading = false);
  }
}

由于模拟器不支持相机调用,只能使用真机,此处为真机截图。

image-20260203141228103

实现相册单选:

XFile? _galleryImage;

Future<void> _pickFromGallery() async {
  setState(() => _isLoading = true);
  try {
    final XFile? image = await _imagePickerService.pickImageFromGallery(
      imageQuality: 85,
    );
    if (image != null) {
      setState(() {
        _galleryImage = image;
      });
      _showSuccessSnackBar('🖼️ 相册选择成功!');
    }
  } catch (e) {
    _showErrorSnackBar('相册选择失败: $e');
  } finally {
    setState(() => _isLoading = false);
  }
}

image-20260203141518296

实现相册多选:

List<XFile> _multipleImages = [];

Future<void> _pickMultipleFromGallery() async {
  setState(() => _isLoading = true);
  try {
    final List<XFile> images = await _imagePickerService.pickMultipleImages(
      imageQuality: 85,
    );
    if (images.isNotEmpty) {
      setState(() {
        _multipleImages = images;
      });
      _showSuccessSnackBar('📚 选择了 ${images.length} 张图片!');
    }
  } catch (e) {
    _showErrorSnackBar('多图选择失败: $e');
  } finally {
    setState(() => _isLoading = false);
  }
}

image-20260203141625555

实现图片预览:

在鸿蒙系统上,与 file_selector 类似,我们同样使用 文件流(Bytes) 的方式来加载图片。

Widget _buildImagePreview(XFile image) {
  return FutureBuilder<Uint8List>(
    future: image.readAsBytes(), // 👈 关键:读取文件流
    builder: (context, snapshot) {
      if (snapshot.connectionState == ConnectionState.done && 
          snapshot.data != null) {
        return ClipRRect(
          borderRadius: BorderRadius.circular(12),
          child: Image.memory(
            snapshot.data!, // 使用 Image.memory 展示
            height: 250,
            width: double.infinity,
            fit: BoxFit.cover,
          ),
        );
      } else if (snapshot.hasError) {
        return const Text('❌ 图片加载失败');
      } else {
        return const CircularProgressIndicator();
      }
    },
  );
}

image-20260203141653751

👶 新手小课堂:XFile 是什么?

XFileimage_picker 返回的文件对象类型。你可以把它理解为一个 📦 智能文件盒子

📋 XFile 的属性:

  • 📝 name:文件名(如 photo_123.jpg
  • 📁 path:文件路径(在鸿蒙上可能是虚拟路径)
  • 🏷️ mimeType:文件类型(如 image/jpeg
  • 🔧 readAsBytes():读取文件内容为字节流
  • 📊 length():获取文件大小

为什么返回 XFile 而不是 File?

File 类型是平台特定的(来自 dart:io),在 Web 平台无法使用。而 XFile🌐 跨平台的抽象类型,无论在移动端、Web 端、桌面端都能正常工作!


🚀 4. 配置应用入口:lib/main.dart

最后,我们在应用的主页添加入口,跳转到演示页面。

修改文件lib/main.dart

操作:

  1. 导入演示页面文件
  2. 在按钮列表中添加跳转按钮
// 1. 导入头文件
import 'image_picker_demo.dart'; 

// 2. 在 build 方法中添加跳转按钮
ElevatedButton(
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => const ImagePickerDemoPage(),
      ),
    );
  },
  child: const Text('Go to Image Picker Demo'),
),

⚠️ 5. 常见错误与解决方案

❌ 错误 1:调用相机或相册无响应

😱 错误现象:

  • 点击按钮后没有任何反应
  • 控制台没有错误提示
  • 系统权限弹窗没有出现

🔍 原因分析:

  • ❌ 应用没有相机或存储权限
  • ❌ 鸿蒙端未正确配置权限声明
  • ❌ 插件未正确注册到 Flutter 引擎

✅ 解决方案:

在鸿蒙项目中配置权限(ohos/entry/src/main/module.json5):

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_permission_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "$string:read_media_permission_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.WRITE_MEDIA",
        "reason": "$string:write_media_permission_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

💡 注意:鸿蒙适配版本的 image_picker 通常已经自动处理权限请求,但如果遇到问题,可以手动添加。

🛠️ 权限说明:

  • 📷 ohos.permission.CAMERA:访问相机拍照
  • 📚 ohos.permission.READ_MEDIA:读取相册图片
  • ✍️ ohos.permission.WRITE_MEDIA:写入拍照照片

❌ 错误 2:图片加载失败或显示错误

😱 错误现象:

Error loading image
或
ImageCodec error

原因分析:

  • ❌ 文件路径不正确
  • ❌ 文件权限问题
  • ❌ 使用了 Image.file() 而不是 Image.memory()

✅ 解决方案:

在鸿蒙系统上,必须使用 readAsBytes() + Image.memory() 的方式

// ❌ 错误:直接使用路径(鸿蒙上可能无法访问)
Image.file(File(xfile.path))

// ✅ 正确:使用文件流
FutureBuilder<Uint8List>(
  future: xfile.readAsBytes(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Image.memory(snapshot.data!);
    }
    return CircularProgressIndicator();
  },
)

❌ 错误 3:多图选择只返回一张图片

😱 错误现象:

  • 调用 pickMultiImage() 但只能选一张

🔍 原因分析:

  • ❌ 使用了 pickImage() 而不是 pickMultiImage()
  • ❌ 鸿蒙系统版本过低,不支持多选

✅ 解决方案:

确保使用正确的方法:

// ✅ 正确:多图选择
final List<XFile> images = await ImagePicker().pickMultiImage(
  imageQuality: 85,
);

// ❌ 错误:单图选择(即使选择多次也只有一张)
final XFile? image = await ImagePicker().pickImage(
  source: ImageSource.gallery,
);

❌ 错误 4:相机拍照后图片很大

😱 错误现象:

  • 拍照后图片文件达到 5-10MB
  • 上传服务器超时或失败

🔍 原因分析:

  • ❌ 没有设置 imageQuality 参数
  • ❌ 没有设置 maxWidthmaxHeight 限制

✅ 解决方案:

合理设置压缩参数:

// ✅ 推荐配置
final XFile? image = await ImagePicker().pickImage(
  source: ImageSource.camera,
  imageQuality: 85,      // 质量:85%(0-100)
  maxWidth: 1920,        // 最大宽度
  maxHeight: 1080,       // 最大高度
);

质量参数参考:

  • 🟢 85-95:高质量,适合打印或高清展示(2-5MB)
  • 🟡 70-85:中等质量,适合普通展示和上传(500KB-2MB)
  • 🟠 50-70:低质量,适合缩略图或快速上传(100-500KB)
  • 🔴 < 50:很低质量,不推荐使用

❌ 错误 5:Flutter 报 XFile not found

😱 错误现象:

Error: Type 'XFile' not found

原因分析:

  • ❌ 没有导入 image_picker
  • ❌ 导入路径错误

✅ 解决方案:

确保正确导入:

import 'package:image_picker/image_picker.dart'; // ✅ 正确导入
import 'dart:typed_data'; // 用于 Uint8List

🎉 结语

通过以上步骤,我们完成了 image_picker 在鸿蒙系统上的完整集成!🚀

📝 本教程涵盖内容:

  • ✅ 引入依赖配置
  • ✅ 二次封装服务类(单例模式)
  • ✅ 相机拍照功能
  • ✅ 相册单选/多选功能
  • ✅ 图片信息获取
  • ✅ 常见错误解决方案

✨ 核心要点回顾:

  • 🎯 二次封装:提高代码复用性和可维护性
  • 🔒 文件流读取:使用 readAsBytes() + Image.memory() 兼容鸿蒙沙箱
  • 📊 质量控制:合理设置 imageQualitymaxWidthmaxHeight 参数
  • 🛡️ 异常处理:使用 try-catch 捕获并处理错误
  • 🎨 用户体验:添加加载状态、提示信息、清空功能

📚 完整实现流程:

📷 相机拍照

🖼️ 相册单选

📚 相册多选

📚 开始

📦 引入image_picker依赖

🛠️ 二次封装ImagePickerService

📱 创建UI演示页面

🎯 用户操作

调用pickImageFromCamera

调用pickImageFromGallery

调用pickMultipleImages

📄 返回XFile对象

📖 readAsBytes读取文件流

✨ Image.memory显示图片

🎉 完成

🎉 祝你开发顺利! 🚀
欢迎加入开源鸿蒙跨平台社区

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐