分享功能
源:Android ShareSheet | iOS UIActivityViewController
系统分享是用户将内容分享到其他应用的标准方式。本文将展示如何跨平台调用系统分享面板。
平台差异对比
| 平台 | 原生 API | 支持内容 | 权限要求 | 特性 |
|---|---|---|---|---|
| Android | ACTION_SEND Intent | 文本、图片、文件、URL | 无需权限 | ShareSheet UI、自定义目标 |
| iOS | UIActivityViewController | 文本、图片、URL、数据 | 无需权限 | 原生分享面板、扩展支持 |
| Desktop | 无原生支持 | - | - | 可复制到剪贴板 |
expect/actual 实现方案
API 核心签名说明
data class ShareContentexpect class ShareManagersuspend fun ShareManager.shareText(text: String, title: String)suspend fun ShareManager.shareImage(imagePath: String, title: String)suspend fun ShareManager.shareUrl(url: String, title: String)
标准代码块
kotlin
data class ShareContent(
val text: String? = null,
val imageUri: String? = null,
val url: String? = null,
val title: String = "",
val subject: String? = null
)
expect class ShareManager {
suspend fun shareText(text: String, title: String = "分享")
suspend fun shareUrl(url: String, title: String = "分享链接")
suspend fun shareImage(imagePath: String, title: String = "分享图片")
suspend fun share(content: ShareContent)
}
// 业务层使用
class SocialShareHelper(private val shareManager: ShareManager) {
suspend fun shareArticle(title: String, url: String) {
val content = "$title\n$url"
shareManager.shareText(content, "分享文章")
}
suspend fun shareInviteCode(code: String) {
val text = "我的邀请码:$code,快来加入吧!"
shareManager.shareText(text, "分享邀请码")
}
suspend fun shareScreenshot(imagePath: String) {
shareManager.shareImage(imagePath, "分享截图")
}
}kotlin
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import java.io.File
actual class ShareManager(private val context: Context) {
actual suspend fun shareText(text: String, title: String) {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, title)
shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(shareIntent)
}
actual suspend fun shareUrl(url: String, title: String) {
shareText(url, title)
}
actual suspend fun shareImage(imagePath: String, title: String) {
val imageFile = File(imagePath)
val imageUri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
imageFile
)
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, imageUri)
type = "image/*"
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
val chooserIntent = Intent.createChooser(shareIntent, title)
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(chooserIntent)
}
actual suspend fun share(content: ShareContent) {
val intent = Intent().apply {
action = Intent.ACTION_SEND
content.text?.let {
putExtra(Intent.EXTRA_TEXT, it)
type = "text/plain"
}
content.subject?.let {
putExtra(Intent.EXTRA_SUBJECT, it)
}
content.imageUri?.let { path ->
val file = File(path)
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
putExtra(Intent.EXTRA_STREAM, uri)
type = "image/*"
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
val chooser = Intent.createChooser(intent, content.title)
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(chooser)
}
}kotlin
import platform.UIKit.*
import platform.Foundation.*
actual class ShareManager {
actual suspend fun shareText(text: String, title: String) {
val items = listOf<Any>(text)
showActivityViewController(items)
}
actual suspend fun shareUrl(url: String, title: String) {
val nsUrl = NSURL.URLWithString(url)
if (nsUrl != null) {
val items = listOf<Any>(nsUrl)
showActivityViewController(items)
}
}
actual suspend fun shareImage(imagePath: String, title: String) {
val image = UIImage.imageWithContentsOfFile(imagePath)
if (image != null) {
val items = listOf<Any>(image)
showActivityViewController(items)
}
}
actual suspend fun share(content: ShareContent) {
val items = mutableListOf<Any>()
content.text?.let { items.add(it) }
content.url?.let {
NSURL.URLWithString(it)?.let { url -> items.add(url) }
}
content.imageUri?.let {
UIImage.imageWithContentsOfFile(it)?.let { image -> items.add(image) }
}
if (items.isNotEmpty()) {
showActivityViewController(items)
}
}
private fun showActivityViewController(items: List<Any>) {
val activityVC = UIActivityViewController(
activityItems = items,
applicationActivities = null
)
val rootVC = UIApplication.sharedApplication.keyWindow?.rootViewController
rootVC?.presentViewController(activityVC, animated = true, completion = null)
}
}kotlin
actual class ShareManager {
actual suspend fun shareText(text: String, title: String) {
// Desktop 降级为复制到剪贴板
ClipboardManager.copyText(text)
println("✅ 已复制到剪贴板: $text")
}
actual suspend fun shareUrl(url: String, title: String) {
ClipboardManager.copyText(url)
println("✅ 已复制链接到剪贴板")
}
actual suspend fun shareImage(imagePath: String, title: String) {
println("⚠️ Desktop 不支持图片分享")
}
actual suspend fun share(content: ShareContent) {
content.text?.let { shareText(it, content.title) }
?: content.url?.let { shareUrl(it, content.title) }
}
}代码封装示例
以下是带分享结果回调的完整封装:
kotlin
// commonMain
sealed class ShareResult {
object Success : ShareResult()
object Canceled : ShareResult()
data class Error(val message: String) : ShareResult()
}
interface ShareCallback {
fun onShareComplete(result: ShareResult)
}
// 实际应用中可通过回调或 Flow 返回结果
class ShareWithCallback(
private val manager: ShareManager,
private val callback: ShareCallback
) {
suspend fun shareWithResult(content: ShareContent) {
try {
manager.share(content)
callback.onShareComplete(ShareResult.Success)
} catch (e: Exception) {
callback.onShareComplete(ShareResult.Error(e.message ?: "Unknown error"))
}
}
}依赖补充
Android FileProvider 配置
需要在 AndroidManifest.xml 配置 FileProvider:
xml
<application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>res/xml/file_paths.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="images" path="/" />
<cache-path name="cache" path="/" />
</paths>实战坑点
Android 7.0 文件 URI 限制
FileUriExposedException
Android 7.0+ 直接分享 file:// URI 会抛出异常。
解决方案:
使用 FileProvider 生成 content:// URI:
kotlin
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)iOS iPad Popover 要求
iPad 崩溃
iPad 上显示 UIActivityViewController 必须设置 sourceView 或 barButtonItem。
解决方案:
kotlin
// iosMain
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
activityVC.popoverPresentationController?.sourceView = rootVC.view
activityVC.popoverPresentationController?.sourceRect = CGRectMake(0.0, 0.0, 1.0, 1.0)
}Android Intent 大小限制
::: caution 数据大小限制 通过 Intent 传递的数据不能超过 1MB(Binder 限制)。 :::
解决方案:
大文件通过 FileProvider 共享,而非直接传递字节数组。
分享失败无回调
用户取消
用户取消分享时,Android/iOS 不会回调失败。
说明:
系统分享面板设计为"发射后不管",无法准确知道分享是否成功。
图片格式兼容性
推荐格式
分享图片推荐使用 JPEG 格式,兼容性最好。
转换示例:
kotlin
// Android
fun convertToJpeg(inputPath: String, outputPath: String) {
val bitmap = BitmapFactory.decodeFile(inputPath)
FileOutputStream(outputPath).use { out ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
}
}