You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Kotlin Multiplatform项目中实现相册图片选择功能

实现KMM跨平台相册图片选择功能

1. 共享模块定义通用接口

在共享模块的commonMain源码集中,定义expect接口抽象图片选择能力,用挂起函数适配异步场景:

// commonMain/kotlin/com/yourpackage/ImagePicker.kt
expect class ImagePicker {
    suspend fun pickImageFromGallery(): ImageResult?
}

// 自定义图片结果数据类,按需扩展字段
data class ImageResult(
    val bytes: ByteArray,
    val mimeType: String,
    val width: Int,
    val height: Int
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as ImageResult

        if (!bytes.contentEquals(other.bytes)) return false
        if (mimeType != other.mimeType) return false
        if (width != other.width) return false
        if (height != other.height) return false

        return true
    }

    override fun hashCode(): Int {
        var result = bytes.contentHashCode()
        result = 31 * result + mimeType.hashCode()
        result = 31 * result + width
        result = 31 * result + height
        return result
    }
}

2. Android平台实现

在androidMain源码集中实现actual类,利用Jetpack Compose的系统相册选择器:

先配置权限

在AndroidManifest.xml中添加权限声明:

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" android:minSdkVersion="33" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

实现ImagePicker

// androidMain/kotlin/com/yourpackage/ImagePicker.kt
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.ByteArrayOutputStream
import kotlin.coroutines.resume

actual class ImagePicker(private val context: Context) {
    actual suspend fun pickImageFromGallery(): ImageResult? {
        return suspendCancellableCoroutine { continuation ->
            val activity = context.findActivity() ?: run {
                continuation.resume(null)
                return@suspendCancellableCoroutine
            }

            val launcher = activity.registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? ->
                uri ?: run {
                    continuation.resume(null)
                    return@registerForActivityResult
                }

                try {
                    context.contentResolver.openInputStream(uri).use { inputStream ->
                        val bitmap = BitmapFactory.decodeStream(inputStream)
                        val baos = ByteArrayOutputStream()
                        bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos)
                        val bytes = baos.toByteArray()

                        val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg"
                        continuation.resume(ImageResult(bytes, mimeType, bitmap.width, bitmap.height))
                    }
                } catch (e: Exception) {
                    continuation.resume(null)
                }
            }

            launcher.launch(ActivityResultContracts.PickVisualMedia.VisualMediaType.ImageOnly)
            continuation.invokeOnCancellation { launcher.unregister() }
        }
    }
}

// 扩展函数:从Context获取当前Activity
fun Context.findActivity(): ComponentActivity? {
    var ctx = this
    while (ctx is ContextWrapper) {
        if (ctx is ComponentActivity) return ctx
        ctx = ctx.baseContext
    }
    return null
}

在Composable中调用

修改AddChildScreen,注入ImagePicker并处理选择结果:

@Composable
fun AddChildScreen(
    viewModel: UserViewModel,
    imagePicker: ImagePicker,
    onBackClicked: () -> Unit
) {
    val coroutineScope = rememberCoroutineScope()
    
    // ... 其他UI组件

    Column(
        modifier = Modifier.padding(15.dp),
        horizontalAlignment = Alignment.Start
    ) {
        ProfilePhoto(
            photoHref = null,
            onEditClicked = {
                coroutineScope.launch {
                    imagePicker.pickImageFromGallery()?.let { result ->
                        viewModel.updateProfilePhoto(result.bytes)
                    }
                }
            }
        )

        // ... 其他UI组件
    }
}

3. iOS平台实现

在iosMain源码集中实现actual类,利用iOS原生的UIImagePickerController:

配置权限描述

在Info.plist中添加相册访问权限说明:

<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以选择头像</string>

实现ImagePicker

// iosMain/kotlin/com/yourpackage/ImagePicker.kt
import platform.UIKit.*
import platform.Foundation.NSData
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume

actual class ImagePicker {
    actual suspend fun pickImageFromGallery(): ImageResult? {
        return suspendCancellableCoroutine { continuation ->
            val picker = UIImagePickerController().apply {
                sourceType = UIImagePickerControllerSourceTypePhotoLibrary
                mediaTypes = arrayOf(kUTTypeImage)
                delegate = object : UIImagePickerControllerDelegateProtocol, UINavigationControllerDelegateProtocol {
                    override fun imagePickerController(
                        picker: UIImagePickerController,
                        didFinishPickingMediaWithInfo: Map<Any?, *>
                    ) {
                        picker.dismissViewControllerAnimated(true, null)
                        val image = didFinishPickingMediaWithInfo[UIImagePickerControllerOriginalImage] as? UIImage ?: run {
                            continuation.resume(null)
                            return
                        }

                        val data = image.jpegData(0.8) as? NSData ?: run {
                            continuation.resume(null)
                            return
                        }

                        continuation.resume(
                            ImageResult(
                                bytes = data.toByteArray(),
                                mimeType = "image/jpeg",
                                width = image.size.width.toInt(),
                                height = image.size.height.toInt()
                            )
                        )
                    }

                    override fun imagePickerControllerDidCancel(picker: UIImagePickerController) {
                        picker.dismissViewControllerAnimated(true, null)
                        continuation.resume(null)
                    }
                }
            }

            UIApplication.sharedApplication.keyWindow?.rootViewController?.presentViewController(picker, animated = true, completion = null)
            continuation.invokeOnCancellation { picker.dismissViewControllerAnimated(true, null) }
        }
    }
}

在iOS入口注入使用

// iOS端主入口
fun MainViewController() = ComposeUIViewController {
    val imagePicker = ImagePicker()
    AddChildScreen(
        viewModel = UserViewModel(),
        imagePicker = imagePicker,
        onBackClicked = { /* 处理返回逻辑 */ }
    )
}

注意事项

  • 权限处理:Android和iOS必须配置对应权限及描述,否则会崩溃或无法上架
  • 图片压缩:示例采用JPEG 80%质量压缩,可按需调整格式和质量
  • 生命周期:协程取消时需关闭选择器,避免内存泄漏
  • 依赖注入:推荐用Koin等框架管理ImagePicker实例,避免直接在Composable中创建

内容的提问来源于stack exchange,提问作者Ibtihaj

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 20:43:15