从零实现一个支持 Android/iOS/HarmonyOS 的图片选择插件

前言

图片选择是移动应用的基础功能,但三个平台的实现方式完全不同。这篇文章我会带你从零实现一个完整的图片选择插件,支持:

  • 从相册选择图片
  • 拍照获取图片
  • 图片压缩和尺寸限制

通过这个实战案例,你会深入理解 Flutter 插件的架构设计和跨平台适配的核心技术。
请添加图片描述

一、插件架构设计

1.1 分层架构

Flutter 官方插件采用联邦插件(Federated Plugin)架构,分为三层:

image_picker/                    # 应用层(用户调用)
  └─ 依赖 image_picker_platform_interface

image_picker_platform_interface/ # 接口层(定义规范)
  └─ 定义抽象接口

image_picker_android/            # Android 实现层
image_picker_ios/                # iOS 实现层
image_picker_ohos/               # HarmonyOS 实现层
  └─ 实现 platform_interface 定义的接口

为什么要分层?

  1. 解耦:应用层不直接依赖平台实现
  2. 扩展性:新增平台只需实现接口
  3. 可测试:可以 mock 接口层进行测试

1.2 目录结构

packages/image_picker/
├── image_picker/                      # 应用层
│   ├── lib/
│   │   └── image_picker.dart          # 统一 API
│   └── pubspec.yaml
│
├── image_picker_platform_interface/   # 接口层
│   ├── lib/
│   │   ├── image_picker_platform_interface.dart
│   │   └── method_channel_image_picker.dart
│   └── pubspec.yaml
│
├── image_picker_android/              # Android 实现
│   ├── android/
│   │   └── src/main/kotlin/
│   │       └── ImagePickerPlugin.kt
│   ├── lib/
│   │   └── image_picker_android.dart
│   └── pubspec.yaml
│
├── image_picker_ios/                  # iOS 实现
│   ├── ios/
│   │   └── Classes/
│   │       └── ImagePickerPlugin.swift
│   ├── lib/
│   │   └── image_picker_ios.dart
│   └── pubspec.yaml
│
└── image_picker_ohos/                 # HarmonyOS 实现
    ├── ohos/
    │   └── src/main/ets/
    │       └── ImagePickerPlugin.ets
    ├── lib/
    │   └── image_picker_ohos.dart
    └── pubspec.yaml

二、接口层设计

2.1 定义抽象接口

// packages/image_picker_platform_interface/lib/image_picker_platform_interface.dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

/// 图片来源
enum ImageSource {
  /// 相机
  camera,
  /// 相册
  gallery,
}

/// 相机设备
enum CameraDevice {
  /// 后置摄像头
  rear,
  /// 前置摄像头
  front,
}

/// 图片选择平台接口
abstract class ImagePickerPlatform extends PlatformInterface {
  ImagePickerPlatform() : super(token: _token);

  static final Object _token = Object();
  static ImagePickerPlatform _instance = MethodChannelImagePicker();

  /// 当前平台实现实例
  static ImagePickerPlatform get instance => _instance;

  /// 设置平台实现(用于测试)
  static set instance(ImagePickerPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  /// 选择单张图片
  ///
  /// [source] 图片来源(相机或相册)
  /// [maxWidth] 最大宽度(像素)
  /// [maxHeight] 最大高度(像素)
  /// [imageQuality] 图片质量(0-100)
  /// [preferredCameraDevice] 优先使用的相机
  Future<XFile?> pickImage({
    required ImageSource source,
    double? maxWidth,
    double? maxHeight,
    int? imageQuality,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
  }) {
    throw UnimplementedError('pickImage() has not been implemented.');
  }

  /// 选择多张图片(仅相册)
  Future<List<XFile>> pickMultiImage({
    double? maxWidth,
    double? maxHeight,
    int? imageQuality,
  }) {
    throw UnimplementedError('pickMultiImage() has not been implemented.');
  }

  /// 选择视频
  Future<XFile?> pickVideo({
    required ImageSource source,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
    Duration? maxDuration,
  }) {
    throw UnimplementedError('pickVideo() has not been implemented.');
  }
}

/// 跨平台文件类
class XFile {
  /// 文件路径
  final String path;

  /// 文件名
  final String? name;

  /// MIME 类型
  final String? mimeType;

  XFile(
    this.path, {
    this.name,
    this.mimeType,
  });

  /// 读取文件字节
  Future<Uint8List> readAsBytes() async {
    // 实现文件读取
    throw UnimplementedError();
  }

  /// 获取文件长度
  Future<int> length() async {
    // 实现获取文件大小
    throw UnimplementedError();
  }
}

2.2 MethodChannel 默认实现

// packages/image_picker_platform_interface/lib/method_channel_image_picker.dart

import 'package:flutter/services.dart';
import 'image_picker_platform_interface.dart';

/// 基于 MethodChannel 的默认实现
class MethodChannelImagePicker extends ImagePickerPlatform {
  
  final MethodChannel channel = const MethodChannel('plugins.flutter.io/image_picker');

  
  Future<XFile?> pickImage({
    required ImageSource source,
    double? maxWidth,
    double? maxHeight,
    int? imageQuality,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
  }) async {
    final String? path = await channel.invokeMethod<String>(
      'pickImage',
      <String, dynamic>{
        'source': source.index,
        'maxWidth': maxWidth,
        'maxHeight': maxHeight,
        'imageQuality': imageQuality,
        'cameraDevice': preferredCameraDevice.index,
      },
    );

    return path != null ? XFile(path) : null;
  }

  
  Future<List<XFile>> pickMultiImage({
    double? maxWidth,
    double? maxHeight,
    int? imageQuality,
  }) async {
    final List<dynamic>? paths = await channel.invokeMethod<List<dynamic>>(
      'pickMultiImage',
      <String, dynamic>{
        'maxWidth': maxWidth,
        'maxHeight': maxHeight,
        'imageQuality': imageQuality,
      },
    );

    if (paths == null) {
      return [];
    }

    return paths.map((dynamic path) => XFile(path as String)).toList();
  }

  
  Future<XFile?> pickVideo({
    required ImageSource source,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
    Duration? maxDuration,
  }) async {
    final String? path = await channel.invokeMethod<String>(
      'pickVideo',
      <String, dynamic>{
        'source': source.index,
        'cameraDevice': preferredCameraDevice.index,
        'maxDuration': maxDuration?.inSeconds,
      },
    );

    return path != null ? XFile(path) : null;
  }
}

三、应用层封装

3.1 用户友好的 API

// packages/image_picker/lib/image_picker.dart

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

export 'package:image_picker_platform_interface/image_picker_platform_interface.dart'
    show ImageSource, CameraDevice, XFile;

/// 图片选择器
class ImagePicker {
  /// 平台接口实例
  static ImagePickerPlatform get _platform => ImagePickerPlatform.instance;

  /// 选择单张图片
  ///
  /// 示例:
  /// ```dart
  /// final picker = ImagePicker();
  /// final XFile? image = await picker.pickImage(source: ImageSource.gallery);
  /// ```
  Future<XFile?> pickImage({
    required ImageSource source,
    double? maxWidth,
    double? maxHeight,
    int? imageQuality,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
  }) {
    return _platform.pickImage(
      source: source,
      maxWidth: maxWidth,
      maxHeight: maxHeight,
      imageQuality: imageQuality,
      preferredCameraDevice: preferredCameraDevice,
    );
  }

  /// 选择多张图片(仅相册)
  ///
  /// 示例:
  /// ```dart
  /// final picker = ImagePicker();
  /// final List<XFile> images = await picker.pickMultiImage();
  /// ```
  Future<List<XFile>> pickMultiImage({
    double? maxWidth,
    double? maxHeight,
    int? imageQuality,
  }) {
    return _platform.pickMultiImage(
      maxWidth: maxWidth,
      maxHeight: maxHeight,
      imageQuality: imageQuality,
    );
  }

  /// 选择视频
  Future<XFile?> pickVideo({
    required ImageSource source,
    CameraDevice preferredCameraDevice = CameraDevice.rear,
    Duration? maxDuration,
  }) {
    return _platform.pickVideo(
      source: source,
      preferredCameraDevice: preferredCameraDevice,
      maxDuration: maxDuration,
    );
  }
}

本篇小结(第一部分)

这部分我们完成了:

  1. ✅ 插件架构设计(联邦插件模式)
  2. ✅ 接口层设计(抽象接口定义)
  3. ✅ MethodChannel 默认实现
  4. ✅ 应用层 API 封装

下一部分我会继续讲解 Android、iOS、HarmonyOS 三端的具体实现。


四、Android 平台实现

4.1 Kotlin 插件代码

// packages/image_picker_android/android/src/main/kotlin/ImagePickerPlugin.kt

package io.flutter.plugins.imagepicker

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
import androidx.core.content.FileProvider
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry
import java.io.File

class ImagePickerPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
    PluginRegistry.ActivityResultListener {

    private lateinit var channel: MethodChannel
    private var activity: Activity? = null
    private var pendingResult: Result? = null
    private var imageUri: Uri? = null

    companion object {
        private const val CHANNEL = "plugins.flutter.io/image_picker"
        private const val REQUEST_CODE_PICK = 2342
        private const val REQUEST_CODE_CAMERA = 2343
    }

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(binding.binaryMessenger, CHANNEL)
        channel.setMethodCallHandler(this)
    }

    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
            "pickImage" -> {
                val source = call.argument<Int>("source") ?: 0
                val maxWidth = call.argument<Double>("maxWidth")
                val maxHeight = call.argument<Double>("maxHeight")
                val imageQuality = call.argument<Int>("imageQuality")

                if (source == 0) {
                    // 相机
                    pickImageFromCamera(result)
                } else {
                    // 相册
                    pickImageFromGallery(result)
                }
            }
            "pickMultiImage" -> {
                pickMultiImage(result)
            }
            else -> {
                result.notImplemented()
            }
        }
    }

    private fun pickImageFromGallery(result: Result) {
        if (activity == null) {
            result.error("NO_ACTIVITY", "Plugin not attached to an activity", null)
            return
        }

        pendingResult = result

        val intent = Intent(Intent.ACTION_PICK).apply {
            type = "image/*"
        }

        activity?.startActivityForResult(intent, REQUEST_CODE_PICK)
    }

    private fun pickImageFromCamera(result: Result) {
        if (activity == null) {
            result.error("NO_ACTIVITY", "Plugin not attached to an activity", null)
            return
        }

        pendingResult = result

        // 创建临时文件
        val imageFile = File(activity?.cacheDir, "image_picker_${System.currentTimeMillis()}.jpg")
        imageUri = FileProvider.getUriForFile(
            activity!!,
            "${activity!!.packageName}.fileprovider",
            imageFile
        )

        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
            putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
        }

        activity?.startActivityForResult(intent, REQUEST_CODE_CAMERA)
    }

    private fun pickMultiImage(result: Result) {
        if (activity == null) {
            result.error("NO_ACTIVITY", "Plugin not attached to an activity", null)
            return
        }

        pendingResult = result

        val intent = Intent(Intent.ACTION_PICK).apply {
            type = "image/*"
            putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
        }

        activity?.startActivityForResult(intent, REQUEST_CODE_PICK)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
        if (requestCode == REQUEST_CODE_PICK || requestCode == REQUEST_CODE_CAMERA) {
            if (resultCode == Activity.RESULT_OK) {
                when (requestCode) {
                    REQUEST_CODE_PICK -> {
                        // 相册选择
                        if (data?.clipData != null) {
                            // 多选
                            val paths = mutableListOf<String>()
                            val clipData = data.clipData!!
                            for (i in 0 until clipData.itemCount) {
                                val uri = clipData.getItemAt(i).uri
                                val path = getPathFromUri(uri)
                                if (path != null) {
                                    paths.add(path)
                                }
                            }
                            pendingResult?.success(paths)
                        } else if (data?.data != null) {
                            // 单选
                            val path = getPathFromUri(data.data!!)
                            pendingResult?.success(path)
                        } else {
                            pendingResult?.success(null)
                        }
                    }
                    REQUEST_CODE_CAMERA -> {
                        // 相机拍照
                        val path = imageUri?.let { getPathFromUri(it) }
                        pendingResult?.success(path)
                    }
                }
            } else {
                pendingResult?.success(null)
            }
            pendingResult = null
            return true
        }
        return false
    }

    private fun getPathFromUri(uri: Uri): String? {
        // 将 URI 转换为文件路径
        // 简化实现,实际需要处理各种 URI 格式
        return uri.path
    }

    override fun onAttachedToActivity(binding: ActivityPluginBinding) {
        activity = binding.activity
        binding.addActivityResultListener(this)
    }

    override fun onDetachedFromActivityForConfigChanges() {
        activity = null
    }

    override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
        activity = binding.activity
        binding.addActivityResultListener(this)
    }

    override fun onDetachedFromActivity() {
        activity = null
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
    }
}

关键点:

  1. ActivityAware:需要 Activity 上下文才能启动系统选择器
  2. ActivityResultListener:监听选择结果
  3. FileProvider:Android 7.0+ 需要使用 FileProvider 共享文件

五、iOS 平台实现

5.1 Swift 插件代码

// packages/image_picker_ios/ios/Classes/ImagePickerPlugin.swift

import Flutter
import UIKit
import Photos

public class ImagePickerPlugin: NSObject, FlutterPlugin {
    private var channel: FlutterMethodChannel?
    private var viewController: UIViewController?
    private var pendingResult: FlutterResult?

    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(
            name: "plugins.flutter.io/image_picker",
            binaryMessenger: registrar.messenger()
        )
        let instance = ImagePickerPlugin()
        instance.channel = channel
        registrar.addMethodCallDelegate(instance, channel: channel)
    }

    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        guard let viewController = UIApplication.shared.keyWindow?.rootViewController else {
            result(FlutterError(code: "NO_VIEW_CONTROLLER", message: "No view controller", details: nil))
            return
        }

        self.viewController = viewController
        self.pendingResult = result

        switch call.method {
        case "pickImage":
            guard let args = call.arguments as? [String: Any],
                  let source = args["source"] as? Int else {
                result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil))
                return
            }

            if source == 0 {
                // 相机
                pickImageFromCamera()
            } else {
                // 相册
                pickImageFromGallery()
            }

        case "pickMultiImage":
            pickMultiImage()

        default:
            result(FlutterMethodNotImplemented)
        }
    }

    private func pickImageFromGallery() {
        let picker = UIImagePickerController()
        picker.sourceType = .photoLibrary
        picker.delegate = self
        viewController?.present(picker, animated: true)
    }

    private func pickImageFromCamera() {
        guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
            pendingResult?(FlutterError(code: "CAMERA_NOT_AVAILABLE", message: "Camera not available", details: nil))
            pendingResult = nil
            return
        }

        let picker = UIImagePickerController()
        picker.sourceType = .camera
        picker.delegate = self
        viewController?.present(picker, animated: true)
    }

    private func pickMultiImage() {
        if #available(iOS 14, *) {
            var configuration = PHPickerConfiguration()
            configuration.selectionLimit = 0 // 0 表示无限制
            configuration.filter = .images

            let picker = PHPickerViewController(configuration: configuration)
            picker.delegate = self
            viewController?.present(picker, animated: true)
        } else {
            // iOS 14 以下不支持多选
            pendingResult?(FlutterError(code: "NOT_SUPPORTED", message: "Multi-image picker requires iOS 14+", details: nil))
            pendingResult = nil
        }
    }
}

// MARK: - UIImagePickerControllerDelegate
extension ImagePickerPlugin: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    public func imagePickerController(
        _ picker: UIImagePickerController,
        didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]
    ) {
        picker.dismiss(animated: true)

        guard let image = info[.originalImage] as? UIImage else {
            pendingResult?(nil)
            pendingResult = nil
            return
        }

        // 保存到临时目录
        let tempDir = NSTemporaryDirectory()
        let fileName = "image_picker_\(Date().timeIntervalSince1970).jpg"
        let filePath = (tempDir as NSString).appendingPathComponent(fileName)

        if let data = image.jpegData(compressionQuality: 0.8) {
            try? data.write(to: URL(fileURLWithPath: filePath))
            pendingResult?(filePath)
        } else {
            pendingResult?(nil)
        }

        pendingResult = nil
    }

    public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true)
        pendingResult?(nil)
        pendingResult = nil
    }
}

// MARK: - PHPickerViewControllerDelegate
@available(iOS 14, *)
extension ImagePickerPlugin: PHPickerViewControllerDelegate {
    public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true)

        guard !results.isEmpty else {
            pendingResult?(nil)
            pendingResult = nil
            return
        }

        var paths: [String] = []
        let group = DispatchGroup()

        for result in results {
            group.enter()
            result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] (object, error) in
                defer { group.leave() }

                guard let image = object as? UIImage else { return }

                let tempDir = NSTemporaryDirectory()
                let fileName = "image_picker_\(Date().timeIntervalSince1970)_\(paths.count).jpg"
                let filePath = (tempDir as NSString).appendingPathComponent(fileName)

                if let data = image.jpegData(compressionQuality: 0.8) {
                    try? data.write(to: URL(fileURLWithPath: filePath))
                    paths.append(filePath)
                }
            }
        }

        group.notify(queue: .main) { [weak self] in
            self?.pendingResult?(paths)
            self?.pendingResult = nil
        }
    }
}

关键点:

  1. UIImagePickerController:iOS 系统图片选择器
  2. PHPickerViewController:iOS 14+ 的新 API,支持多选
  3. Delegate 模式:通过 delegate 回调获取结果

六、HarmonyOS 平台实现

6.1 ArkTS 插件代码

// packages/image_picker_ohos/ohos/src/main/ets/ImagePickerPlugin.ets

import { FlutterPlugin, MethodCall, MethodChannel, MethodResult } from '@ohos/flutter_ohos';
import picker from '@ohos.file.picker';
import fs from '@ohos.file.fs';
import { BusinessError } from '@ohos.base';

export class ImagePickerPlugin implements FlutterPlugin {
  private channel: MethodChannel | null = null;

  getUniqueClassName(): string {
    return "ImagePickerPlugin";
  }

  onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding): void {
    this.channel = new MethodChannel(
      binding.getBinaryMessenger(),
      'plugins.flutter.io/image_picker'
    );
    this.channel.setMethodCallHandler(this.onMethodCall.bind(this));
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    const method = call.method;

    if (method === 'pickImage') {
      this.pickImage(call, result);
    } else if (method === 'pickMultiImage') {
      this.pickMultiImage(call, result);
    } else {
      result.notImplemented();
    }
  }

  private pickImage(call: MethodCall, result: MethodResult): void {
    try {
      const args = call.args as Record<string, any>;
      const source = args['source'] as number;

      if (source === 0) {
        // 相机
        result.error('NOT_IMPLEMENTED', '相机功能暂未实现', null);
      } else {
        // 相册
        this.pickImageFromGallery(result);
      }
    } catch (err) {
      result.error('ERROR', (err as Error).message, null);
    }
  }

  private pickImageFromGallery(result: MethodResult): void {
    try {
      // 创建图片选择器配置
      const photoSelectOptions = new picker.PhotoSelectOptions();
      photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
      photoSelectOptions.maxSelectNumber = 1;

      // 创建图片选择器
      const photoPicker = new picker.PhotoViewPicker();

      // 启动选择器
      photoPicker.select(photoSelectOptions).then((selectResult: picker.PhotoSelectResult) => {
        if (selectResult.photoUris.length > 0) {
          const uri = selectResult.photoUris[0];
          // 将 URI 转换为路径
          const path = this.convertUriToPath(uri);
          result.success(path);
        } else {
          result.success(null);
        }
      }).catch((err: BusinessError) => {
        console.error(`[ImagePicker] 选择图片失败: ${err.message}`);
        result.error('PICK_ERROR', err.message, null);
      });
    } catch (err) {
      result.error('ERROR', (err as Error).message, null);
    }
  }

  private pickMultiImage(call: MethodCall, result: MethodResult): void {
    try {
      const args = call.args as Record<string, any>;
      const maxWidth = args['maxWidth'] as number;
      const maxHeight = args['maxHeight'] as number;

      // 创建图片选择器配置
      const photoSelectOptions = new picker.PhotoSelectOptions();
      photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
      photoSelectOptions.maxSelectNumber = 9; // 最多选择 9 张

      // 创建图片选择器
      const photoPicker = new picker.PhotoViewPicker();

      // 启动选择器
      photoPicker.select(photoSelectOptions).then((selectResult: picker.PhotoSelectResult) => {
        const paths: string[] = [];
        for (const uri of selectResult.photoUris) {
          const path = this.convertUriToPath(uri);
          paths.push(path);
        }
        result.success(paths);
      }).catch((err: BusinessError) => {
        console.error(`[ImagePicker] 选择多张图片失败: ${err.message}`);
        result.error('PICK_ERROR', err.message, null);
      });
    } catch (err) {
      result.error('ERROR', (err as Error).message, null);
    }
  }

  private convertUriToPath(uri: string): string {
    // HarmonyOS 的 Picker 返回的是 URI 格式
    // 例如:file://media/Photo/1/IMG_1234.jpg
    if (uri.startsWith('file://')) {
      return uri.substring(7); // 移除 "file://" 前缀
    }
    return uri;
  }

  onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding): void {
    if (this.channel !== null) {
      this.channel.setMethodCallHandler(null);
    }
  }
}

关键点:

  1. Picker API:使用 @ohos.file.picker 模块
  2. Promise 异步:所有操作都是异步的
  3. URI 转换:Picker 返回 URI,需要转换为路径
  4. 错误处理:使用 try-catch 和 Promise.catch

本篇完整小结

这篇文章我们完成了:

  1. ✅ 插件架构设计(联邦插件模式)
  2. ✅ 接口层设计(抽象接口 + MethodChannel)
  3. ✅ 应用层 API 封装
  4. ✅ Android 平台实现(Kotlin)
  5. ✅ iOS 平台实现(Swift)
  6. ✅ HarmonyOS 平台实现(ArkTS)

关键要点:

  • 采用联邦插件架构,分离接口和实现
  • 三端使用各自的系统 API
  • 统一返回文件路径,而非文件内容
  • 做好错误处理和边界情况

思考题

  1. 为什么要返回文件路径而不是文件内容?
  2. 如何实现图片压缩和尺寸限制?
  3. 多选图片时如何优化性能?

下一篇预告:第06篇 - 本地存储的跨平台实现

更多推荐