如何在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
相关产品推荐
相关产品推荐

