Skip to content

分享功能

源:Android ShareSheet | iOS UIActivityViewController

系统分享是用户将内容分享到其他应用的标准方式。本文将展示如何跨平台调用系统分享面板。

平台差异对比

平台原生 API支持内容权限要求特性
AndroidACTION_SEND Intent文本、图片、文件、URL无需权限ShareSheet UI、自定义目标
iOSUIActivityViewController文本、图片、URL、数据无需权限原生分享面板、扩展支持
Desktop无原生支持--可复制到剪贴板

expect/actual 实现方案

API 核心签名说明

  • data class ShareContent
  • expect class ShareManager
  • suspend 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)
    }
}